1
0
mirror of synced 2026-09-04 23:19:38 +00:00

Compare commits

..

51 Commits

Author SHA1 Message Date
Christoph Strobl a2a23820fa Release version 6.2.0-M1 (2026.1.0).
See #3296
2026-08-14 10:51:49 +02:00
Christoph Strobl 8c9dbc90ec Enter rampdown phase for 6.2.0-M1.
See #3296
2026-08-13 09:05:27 +02:00
Peter-Josef Meisch 6d01b5824f Polishing.
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-08-10 17:21:22 +02:00
Mark Paluch 142e385cf2 Upgrade to Elasticsearch REST Client 9.5.0.
See #3325
2026-08-10 15:36:13 +02:00
Mark Paluch b89acfb2bd Update build config for correct notice and license.txt inclusion.
Closes spring-projects/spring-data-commons#3524
2026-08-10 12:09:34 +02:00
Christoph Strobl 854ba1f8a8 Update release train automation.
See: spring-projects/spring-data-build#2891
2026-08-10 07:11:10 +02:00
Christoph Strobl 073bdd3751 Rervert change that disabled testcontainers reuse.
See: spring-projects/spring-data-build#2891
2026-08-06 15:39:44 +02:00
Mark Paluch 20b049abed Add release train automation.
Closes spring-projects/spring-data-build#2891
2026-08-05 16:11:45 +02:00
Peter-Josef Meisch c08dd4bff0 Consolidate use of ValueExpressions insead of Expressions in the SimpleElasticsearchPersitentEntity.
Closes: #3321
Original Pull Request #3323

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-08-02 18:41:24 +02:00
Peter-Josef Meisch 8cc6da4b98 Implement property resolution for index names.
* update index name resolution implementation
* documentation and refactoring

Closes: #3310
Original Pull Request: #3322

---------

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-07-29 07:39:54 +02:00
Steven 29821baed6 New Doc section on Server Type
Original Pull Request: #3320

Signed-off-by: Stexxen <steven.pearce@nowyoyo.com>
2026-07-27 19:51:28 +02:00
Mark Paluch 56eb92e090 Enable private vulnerability reporting.
Closes #3319
2026-07-27 09:16:51 +02:00
林桉 fd557bc45c Optimize DocumentAdapters field conversion.
Convert search hit fields from JsonData through JSON-P values directly instead of building an intermediate JSON string and parsing it back into an EntityAsMap.

This keeps parser-backed fields as regular Java Map/List/String/Number/Boolean/null values and avoids leaking JsonValue implementations into SearchDocument field access.

Closes" #3178
Original Pull Request: #3311

Signed-off-by: 014-code <2402143478@qq.com>
2026-07-26 14:29:48 +02:00
Peter-Josef Meisch 63f3a355d9 Upgrade Elasticsearch to 9.4.4
* Upgrade Elasticsearch to 9.4.4

Closes #3315
Original Pull Request: #3317

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-07-25 15:38:47 +02:00
Peter-Josef Meisch becfe765f5 Polishing 2026-07-25 15:35:00 +02:00
Steven 82be8ffefc Support Elasticsearch Serverless
Closes: #3306
Original Pull Request: #3312

Signed-off-by: Steven <steven.pearce@nowyoyo.com>
2026-07-25 15:00:49 +02:00
Sanghyuk Jung f01f797296 Remove duplicated "the"
Original Pull Request: #3314

Signed-off-by: Sanghyuk Jung <sanghyuk.jung@navercorp.com>
2026-07-25 11:24:50 +02:00
Peter-Josef Meisch 8b067983ed documentation
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-07-25 11:22:22 +02:00
Peter-Josef Meisch eb98fa0bb5 Polishing
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-07-25 11:16:12 +02:00
Andriy Redko c64780be71 Allow to provide non-standard (custom) index options for @Field / @InnerField
Closes #3299
Original Pull Request: #3309

Signed-off-by: Andriy Redko <drreta@gmail.com>
2026-07-25 10:50:41 +02:00
Peter-Josef Meisch 3b21a1882b Polishing 2026-07-13 18:44:49 +02:00
林桉 7e70435f48 Fix async Rest5 client withHeaders() not seeing ThreadLocal auth
Closes #3300

Signed-off-by: 014-code <2402143478@qq.com>
2026-07-13 18:43:41 +02:00
Peter-Josef Meisch 14209bf730 Upgrade Elasticsearch to 9.4.3.
Closes #3301
Closes #3298

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-07-03 21:53:58 +02:00
Mark Paluch 31eee9fae5 Use property to easier configure Spring Framework reference docs location.
See spring-projects/spring-data-build#2886
2026-06-17 16:37:46 +02:00
Christoph Strobl 977b7b23cc Prepare next development iteration.
See #3277
2026-06-09 10:50:10 +02:00
Christoph Strobl 38ba9fd08a Release version 6.1 GA (2026.0.0).
See #3277
2026-06-09 10:50:10 +02:00
Peter-Josef Meisch 473caa8214 Missing parts of update to Elasticseach 9.4.2
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-06-02 20:06:28 +02:00
Mark Paluch 33faaa8689 Upgrade to Maven Wrapper 3.9.16.
See #3291
2026-06-02 14:57:30 +02:00
Mark Paluch 189611af7f Upgrade to Elasticsearch REST Client 9.4.2.
See #3290
2026-06-02 11:42:06 +02:00
Mark Paluch db2f779ff3 Refine GitHub Actions workflows.
See #3277
2026-06-02 09:21:52 +02:00
Peter-Josef Meisch 6654eaa38f Upgrade Elasticsearch client to 9.4.1.
Closes #3288

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-05-22 22:00:04 +02:00
Peter-Josef Meisch ebdc57e4b2 Update documentation.
Closes #3286

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-05-09 17:57:50 +02:00
Peter-Josef Meisch 4cbfef39c6 Upgrade to Elasticsearch 9.4.0.
Closes #3284

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-05-09 17:50:47 +02:00
Peter-Josef Meisch 8ad2e8e0a0 nullability-fixes
Closes #3282

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-05-02 11:20:02 +02:00
Steven 795e1ef60a Refactor routing and null handling with a new getRouting methods. 2026-04-26 08:02:09 +02:00
Peter-Josef Meisch a08ffc85cf fix passing null routing to Elasticsearch's builder.
Closes #3278

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-04-22 20:43:52 +02:00
Mark Paluch c3eb590afe After release cleanups.
See #3258
2026-04-17 17:14:29 +02:00
Mark Paluch fa065c1f27 Prepare next development iteration.
See #3258
2026-04-17 17:14:28 +02:00
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
253 changed files with 4447 additions and 1071 deletions
@@ -0,0 +1,30 @@
name: Build Release
runs:
using: composite
steps:
- name: Write settings.xml
if: ${{ env.RELEASE_TRAIN_SETTINGS_XML != '' }}
shell: bash
run: |
echo "::notice::Writing provided settings-xml input to release-train-settings.xml"
echo "${RELEASE_TRAIN_SETTINGS_XML}" > release-train-settings.xml
- name: Prepare Maven command
shell: bash
run: |
MAVEN_LOGGING_OPTS=()
if [[ "${RUNNER_DEBUG}" == "1" || "${ACTIONS_STEP_DEBUG}" == "true" ]]; then
echo "::notice::transfer-progress enabled"
else
MAVEN_LOGGING_OPTS+=(--no-transfer-progress)
fi
echo "RELEASE_TRAIN_MVN=./mvnw --batch-mode ${MAVEN_LOGGING_OPTS[*]} --settings release-train-settings.xml clean deploy -DaltDeploymentRepository=release-train::file://$(pwd)/deployment-repository -DskipTests -Dgpg.skip" >> "${GITHUB_ENV}"
- name: Build Release
shell: bash
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,release,ci
- name: Distribution build
shell: bash
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,distribute
- name: Schema build
shell: bash
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,distribute-schema
@@ -0,0 +1,6 @@
artifactory:
artifacts:
- pattern: "/**/spring-data-*-docs.zip"
properties:
zip.deployed: "false"
zip.type: "docs"
@@ -0,0 +1,24 @@
name: Test Release
runs:
using: composite
steps:
- name: Write settings.xml
if: ${{ env.RELEASE_TRAIN_SETTINGS_XML != '' }}
shell: bash
run: |
echo "::notice::Writing provided settings-xml input to release-train-settings.xml"
echo "${RELEASE_TRAIN_SETTINGS_XML}" > release-train-settings.xml
- name: Prepare Maven command
shell: bash
run: |
MAVEN_LOGGING_OPTS=()
if [[ "${RUNNER_DEBUG}" == "1" || "${ACTIONS_STEP_DEBUG}" == "true" ]]; then
echo "::notice::transfer-progress enabled"
else
MAVEN_LOGGING_OPTS+=(--no-transfer-progress)
fi
echo "RELEASE_TRAIN_MVN=./mvnw --batch-mode ${MAVEN_LOGGING_OPTS[*]} --settings release-train-settings.xml clean verify -Dgpg.skip" >> "${GITHUB_ENV}"
- name: Test Release
shell: bash
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,release,ci
+26
View File
@@ -0,0 +1,26 @@
workflow:
generator:
project:
java:
versions:
primary: 25
workflows:
release-train:
join:
watch: false
leave:
watch: false
ready:
watch: false
build:
env:
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
RELEASE_TRAIN_SETTINGS_XML: vars.RELEASE_TRAIN_SETTINGS_XML
test:
env:
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
RELEASE_TRAIN_SETTINGS_XML: vars.RELEASE_TRAIN_SETTINGS_XML
+7 -1
View File
@@ -13,7 +13,7 @@ jobs:
matrix:
java-version: [ base, main ]
name: Build project
runs-on: ubuntu-latest
runs-on: ${{ vars.ACTION_RUNNER || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v6
- name: Setup Java and Maven
@@ -24,4 +24,10 @@ jobs:
- name: Build
uses: spring-projects/spring-data-build/actions/maven-build@main
env:
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
COMMERCIAL_ARTIFACTORY_USR: '${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}'
COMMERCIAL_ARTIFACTORY_PSW: '${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}'
TESTCONTAINERS_REUSE_ENABLE: true
with:
settings-xml: '${{ vars.SETTINGS_XML }}'
+1
View File
@@ -4,6 +4,7 @@ name: "CodeQL Advanced"
on:
push:
branches: [ main ]
pull_request:
workflow_dispatch:
schedule:
+92
View File
@@ -0,0 +1,92 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Build"
run-name: "${{ inputs.callback-ref }} Build"
"on":
workflow_dispatch:
inputs:
callback:
description: "Repository to which a callback should be made upon completion"
required: true
type: "string"
callback-ref:
description: "Ref in the callback repository to which a callback should be made upon completion"
required: true
type: "string"
release-train-maven-repository-url:
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
required: true
type: "string"
permissions:
contents: "read"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
jobs:
build-release:
name: "Build Release"
runs-on: "ubuntu22-2-8"
steps:
- name: "Prevent Re-runs"
id: "prevent-re-runs"
run: |-
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
echo "Re-runs are prohibited. Use the 'Release Train  Retry' workflow to retry build failures"
exit 1
fi
- name: "Set up Java"
id: "set-up-java"
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
with:
distribution: "liberica"
java-version: "25"
- name: "Check Out Code"
id: "check-out-code"
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
- name: "Build Release"
id: "build-release"
uses: "./.github/actions/release-train-build"
env:
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
RELEASE_TRAIN_SETTINGS_XML: "${{ vars.RELEASE_TRAIN_SETTINGS_XML }}"
- name: "Upload Deployment Repository"
id: "upload-deployment-repository"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
name: "deployment-repository"
path: "deployment-repository/**"
- name: "Upload Deployment Spec"
id: "upload-deployment-spec"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
archive: "false"
if-no-files-found: "ignore"
name: "deployment-spec"
path: ".github/actions/release-train-build/deployment-spec.yml"
- name: "Save Build System Caches"
id: "save-build-system-caches"
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
with:
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
path: "~/.m2/repository"
- name: "Send Callback"
id: "send-callback"
if: "${{ !cancelled() }}"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run callback \
--repo ${{ inputs.callback }} \
--ref ${{ inputs.callback-ref }} \
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
--field deployment-repository-artifact-identifier=${{ steps.upload-deployment-repository.outputs.artifact-id }} \
--field deployment-spec-artifact-identifier=${{ steps.upload-deployment-spec.outputs.artifact-id }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }} \
--field result=${{ job.status == 'success' && 'built' || 'build-failed' }} \
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+43
View File
@@ -0,0 +1,43 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Join"
run-name: "${{ inputs.release-train }} Join"
"on":
workflow_dispatch:
inputs:
deployment-destination:
description: "Destination to which the release should be deployed"
options:
- "Maven Central"
- "Spring Enterprise"
required: true
type: "choice"
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
join-release-train:
name: "Join Release Train"
runs-on: "ubuntu-latest"
steps:
- name: "Join Release Train"
id: "join-release-train"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run join \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field commit-hash=${{ github.sha }} \
--field deployment-destination=${{ inputs.deployment-destination == 'Maven Central' && 'maven-central' || 'spring-enterprise' }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
+34
View File
@@ -0,0 +1,34 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Leave"
run-name: "${{ inputs.release-train }} Leave"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
leave:
name: "Leave"
runs-on: "ubuntu-latest"
steps:
- name: "Leave"
id: "leave"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run leave \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
+35
View File
@@ -0,0 +1,35 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Ready"
run-name: "${{ inputs.release-train }} Ready"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
ready:
name: "Ready"
runs-on: "ubuntu-latest"
steps:
- name: "Ready"
id: "ready"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run ready \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field commit-hash=${{ github.sha }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
+34
View File
@@ -0,0 +1,34 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Retry"
run-name: "${{ inputs.release-train }} Retry"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
trigger-retry:
name: "Trigger Retry"
runs-on: "ubuntu-latest"
steps:
- name: "Trigger Retry"
id: "trigger-retry"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run retry \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
+85
View File
@@ -0,0 +1,85 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Test"
run-name: "${{ inputs.callback-ref }} Test"
"on":
workflow_dispatch:
inputs:
callback:
description: "Repository to which a callback should be made upon completion"
required: true
type: "string"
callback-ref:
description: "Ref in the callback repository to which a callback should be made upon completion"
required: true
type: "string"
release-train-maven-repository-url:
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
required: true
type: "string"
permissions:
contents: "read"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
jobs:
test-release:
name: "Test Release"
runs-on: "ubuntu22-2-8"
steps:
- name: "Prevent Re-runs"
id: "prevent-re-runs"
run: |-
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
echo "Re-runs are prohibited. Use the 'Release Train  Retry' workflow to retry test failures"
exit 1
fi
- name: "Set up Java"
id: "set-up-java"
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
with:
distribution: "liberica"
java-version: "25"
- name: "Check Out Code"
id: "check-out-code"
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
- name: "Restore Build System Caches"
id: "restore-build-system-caches"
uses: "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
with:
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
path: "~/.m2/repository"
- name: "Test Release"
id: "test-release"
uses: "./.github/actions/release-train-test"
env:
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
RELEASE_TRAIN_SETTINGS_XML: "${{ vars.RELEASE_TRAIN_SETTINGS_XML }}"
- name: "Send Callback"
id: "send-callback"
if: "${{ !cancelled() }}"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run callback \
--repo ${{ inputs.callback }} \
--ref ${{ inputs.callback-ref }} \
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }} \
--field result=${{ job.status == 'success' && 'tested' || 'test-failed' }} \
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "Upload Build System Reports"
id: "upload-build-system-reports"
if: "${{ failure() }}"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
name: "build-system-reports"
path: |-
**/target/surefire-reports
**/target/failsafe-reports
+7 -4
View File
@@ -3,7 +3,7 @@ name: Snapshots
on:
workflow_dispatch:
push:
branches: [ main, 'issue/**' ]
branches: [ main, 'issue/**', 'release/**' ]
permissions: read-all
@@ -11,7 +11,7 @@ jobs:
build-snapshots:
name: Build and deploy snapshots
if: ${{ github.repository_owner == 'spring-projects' }}
runs-on: ubuntu-latest
runs-on: ${{ vars.ACTION_RUNNER || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v6
- name: Setup Java and Maven
@@ -21,8 +21,11 @@ jobs:
- name: Deploy to Artifactory
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@main
env:
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
COMMERCIAL_ARTIFACTORY_USR: '${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}'
COMMERCIAL_ARTIFACTORY_PSW: '${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}'
TESTCONTAINERS_REUSE_ENABLE: true
with:
build-name: 'spring-data-elasticsearch'
username: '${{ secrets.ARTIFACTORY_USERNAME }}'
password: '${{ secrets.ARTIFACTORY_PASSWORD }}'
settings-xml: '${{ vars.SETTINGS_XML }}'
+2 -2
View File
@@ -1,3 +1,3 @@
#Thu Jul 17 13:59:56 CEST 2025
#Tue Jun 02 14:58:35 CEST 2026
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
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
-15
View File
@@ -1,15 +0,0 @@
= Security Policy
== Reporting a Vulnerability
Please, https://github.com/spring-projects/security-advisories/security/advisories/new[open a draft security advisory] if you need to disclose and discuss a security issue in private with the Spring Data team.
Note that we only accept reports against https://spring.io/projects/spring-data#support[supported versions].
For more details, check out our https://spring.io/security-policy[security policy].
== JAR signing
Spring Data JARs released on Maven Central are signed.
You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt
Versions released prior to 2023 may be signed with a different key.
+15
View File
@@ -0,0 +1,15 @@
# Security Policy
## Reporting a Vulnerability
Please [open a draft security advisory](https://github.com/spring-projects/spring-data-elasticsearch/security/advisories/new) if you need to disclose and discuss a security issue in private with the Spring Data team.
Note that we only accept reports against [supported versions](https://spring.io/projects/spring-data#support).
For more details, check out our [security policy](https://spring.io/security-policy).
## JAR signing
Spring Data JARs released on Maven Central are signed.
You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt
Versions released prior to 2023 may be signed with a different key.
+11
View File
@@ -0,0 +1,11 @@
= Getting support for Spring Data Elasticsearch
Spring Data Elasticsearch is a community-maintained Spring Data project. It is released as part of the Spring Data Release train.
== GitHub issues
We choose not to use GitHub issues for general usage questions and support, preferring to use issues solely for the tracking of bugs and enhancements.
If you have a general usage question, please do not open a GitHub issue but use one of the other channels https://github.com/spring-projects/spring-data-elasticsearch#getting-help[described in the readme file].
If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible.
Ideally, that would include a small sample project that reproduces the problem.
+17 -8
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>6.1.0-M2</version>
<version>6.2.0-M1</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>4.1.0-M2</version>
<version>4.2.0-M1</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.1.0-M2</springdata.commons>
<springdata.commons>4.2.0-M1</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>9.3.1</elasticsearch-java>
<elasticsearch-rest-client>9.3.1</elasticsearch-rest-client>
<elasticsearch-java>9.5.0</elasticsearch-java>
<elasticsearch-rest-client>9.5.0</elasticsearch-rest-client>
<hoverfly>0.20.2</hoverfly>
<log4j>2.25.3</log4j>
<log4j>2.25.4</log4j>
<jsonassert>1.5.3</jsonassert>
<wiremock>3.9.2</wiremock>
@@ -82,8 +82,7 @@
<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>
@@ -377,8 +376,18 @@
<filtering>false</filtering>
<excludes>
<exclude>**/versions.properties</exclude>
<exclude>notice.txt</exclude>
<exclude>license.txt</exclude>
</excludes>
</resource>
<resource>
<directory>${project.root}/src/main/resources</directory>
<targetPath>META-INF</targetPath>
<includes>
<include>notice.txt</include>
<include>license.txt</include>
</includes>
</resource>
</resources>
<plugins>
-29
View File
@@ -1,29 +0,0 @@
<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>
@@ -3,7 +3,7 @@
This chapter illustrates configuration and usage of supported Elasticsearch client implementations.
Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node or a cluster.
Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node, a cluster or Elasticsearch serverless.
Although the Elasticsearch Client can be used directly to work with the cluster, applications using Spring Data Elasticsearch normally use the higher level abstractions of xref:elasticsearch/template.adoc[Elasticsearch Operations] and xref:elasticsearch/repositories/elasticsearch-repositories.adoc[Elasticsearch Repositories].
[[elasticsearch.clients.rest5client]]
@@ -454,6 +454,22 @@ ClientConfiguration.builder()
----
====
[[elasticsearch.clients.servertype]]
== Server Type
There are now 2 types of server that a client could connect to. They are `DEFAULT` which represent all single nodes and clusters; and `SERVERLESS` which represents Elasticsearch new flavour of providing cloud hosted elasticsearch.
If you are going to use Elasticsearch Serverless then you need to specific this so that certain settings are changed and indexes are created correctly.
The javadoc:org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport[] class (extended by javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]) allows the overriding of `serverType()`.
====
[source,java]
----
@Override
ElasticsearchServerType serverType() {
return ElasticsearchServerType.SERVERLESS;
}
----
====
[[elasticsearch.clients.logging]]
== Client Logging
@@ -1,10 +1,18 @@
[[new-features]]
= What's new
[[new-features.6-2-0]]
== New in Spring Data Elasticsearch 6.2
* Upgrade to Elasticsearch 9.5.0
* Allow to provide non-standard (custom) index options for `@Field` / `@InnerField`
* Support Elasticsearch Serverless
* Support propert resolution in index name expressions (https://github.com/spring-projects/spring-data-elasticsearch/issues/3310[GH #3310])
[[new-features.6-1-0]]
== New in Spring Data Elasticsearch 6.1
* Upgrade to Elasticsearch 9.3.1
* Upgrade to Elasticsearch 9.4.2
* Add support to use `IndexCoordinates` as repository query parameter
* Add support for includeNamedQueriesScore in Query
* Add support for Micrometer observation.
@@ -22,7 +22,7 @@ The following annotations are available:
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database.
The most important attributes are (check the API documentation for the complete list of attributes):
** `indexName`: the name of the index to store this entity in.
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"`
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"` or a property expression like `{index.name}`
** `createIndex`: flag whether to create an index on repository bootstrapping.
Default value is _true_.
See xref:elasticsearch/repositories/elasticsearch-repositories.adoc#elasticsearch.repositories.autocreation[Automatic creation of indices with the corresponding mapping]
@@ -6,10 +6,11 @@ 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
| 2026.0 | 6.1.x | 9.3.1 | 7.0.x
| 2026.1 | 6.2.x | 9.5.0 | 7.0.x
| 2026.0 | 6.1.x | 9.4.2 | 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
| 2025.0 | 5.5.xfootnote:oom[Out of maintenance] | 8.18.1 | 6.2.x
| 2024.1 | 5.4.xfootnote:oom[] | 8.15.5 | 6.1.x
| 2024.0 | 5.3.xfootnote:oom[] | 8.13.4 | 6.1.x
| 2023.1 (Vaughan) | 5.2.xfootnote:oom[] | 8.11.1 | 6.1.x
| 2023.0 (Ullmann) | 5.1.xfootnote:oom[] | 8.7.1 | 6.0.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]
@@ -13,7 +13,7 @@ asciidoc:
include-xml-namespaces: false
spring-data-commons-docs-url: '${documentation.baseurl}/spring-data/commons/reference/${springdata.commons.short}'
spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java'
springdocsurl: '${documentation.baseurl}/spring-framework/reference/{springversionshort}'
springdocsurl: '${documentation.spring-reference-url}/{springversionshort}'
spring-framework-docs: '{springdocsurl}'
springjavadocurl: '${documentation.spring-javadoc-url}'
spring-framework-javadoc: '{springjavadocurl}'
@@ -15,10 +15,11 @@
*/
package org.springframework.data.elasticsearch;
import org.springframework.dao.DataRetrievalFailureException;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.dao.DataRetrievalFailureException;
/**
* @author Peter-Josef Meisch
* @author Illia Ulianov
@@ -42,6 +43,6 @@ public class BulkFailureException extends DataRetrievalFailureException {
* @author Illia Ulianov
* @since 5.2
*/
public record FailureDetails(Integer status, String errorMessage) {
public record FailureDetails(Integer status, @Nullable String errorMessage) {
}
}
@@ -28,7 +28,7 @@ import org.jspecify.annotations.Nullable;
public class ElasticsearchErrorCause {
@Nullable private final String type;
private final String reason;
@Nullable private final String reason;
@Nullable private final String stackTrace;
@@ -38,7 +38,7 @@ public class ElasticsearchErrorCause {
private final List<ElasticsearchErrorCause> suppressed;
public ElasticsearchErrorCause(@Nullable String type, String reason, @Nullable String stackTrace,
public ElasticsearchErrorCause(@Nullable String type, @Nullable String reason, @Nullable String stackTrace,
@Nullable ElasticsearchErrorCause causedBy, List<ElasticsearchErrorCause> rootCause,
List<ElasticsearchErrorCause> suppressed) {
this.type = type;
@@ -54,7 +54,7 @@ public class ElasticsearchErrorCause {
return type;
}
public String getReason() {
public @Nullable String getReason() {
return reason;
}
@@ -0,0 +1,48 @@
/*
* 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.annotations;
import org.springframework.data.elasticsearch.core.index.IndexOptionMapper;
/**
* Represents the custom index option, either not provided by the core (fe, custom plugin) or deviates across different
* engines.
*
* @author Andriy Redko
* @since 6.2
*/
public @interface CustomIndexOption {
/**
* The name of the custom index option
*/
String name();
/**
* The value(s) of the custom index option
*/
String[] values() default {};
/**
* Should the index property be overridden if already present or not
*/
boolean overrideIfPresent() default false;
/**
* The index option mapper that will be used to populate this custom index option
*/
Class<? extends IndexOptionMapper> mapper();
}
@@ -252,4 +252,11 @@ public @interface Field {
* @since 5.4
*/
String mappedTypeName() default "";
/**
* adds the custom index options for a particular field
*
* @since 6.2
*/
CustomIndexOption[] customIndexOptions() default {};
}
@@ -179,4 +179,11 @@ public @interface InnerField {
* @since 5.4
*/
String mappedTypeName() default "";
/**
* adds the custom index options for a particular field
*
* @since 6.2
*/
CustomIndexOption[] customIndexOptions() default {};
}
@@ -22,12 +22,14 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentEntity;
/**
* Elasticsearch Setting
*
* @author Mohsin Husen
* @author Peter-Josef Meisch
* @author Steven Pearce
*/
@Persistent
@@ -59,9 +61,10 @@ public @interface Setting {
short replicas() default 1;
/**
* Refresh interval for the index. Used for index creation.
* Refresh interval for the index. Used for index creation. If no value, defaults are server type dependant and set in
* {@link SimpleElasticsearchPersistentEntity}
*/
String refreshInterval() default "1s";
String refreshInterval() default "";
/**
* Index storage type for the index. Used for index creation.
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.annotations;
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.aot;
@@ -24,6 +24,20 @@ import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.NestedIdentity;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpMapper;
import jakarta.json.JsonArray;
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
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;
@@ -36,13 +50,6 @@ import org.springframework.data.elasticsearch.core.document.SearchDocumentAdapte
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Utility class to adapt different Elasticsearch responses to a
* {@link org.springframework.data.elasticsearch.core.document.Document}
@@ -54,188 +61,228 @@ import java.util.stream.Collectors;
*/
final class DocumentAdapters {
private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class);
private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class);
private DocumentAdapters() {
}
private DocumentAdapters() {}
/**
* Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client.
*
* @param hit the hit object
* @param jsonpMapper to map JsonData objects
* @return the created {@link SearchDocument}
*/
public static SearchDocument from(Hit<?> hit, JsonpMapper jsonpMapper) {
/**
* Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client.
*
* @param hit the hit object
* @param jsonpMapper to map JsonData objects
* @return the created {@link SearchDocument}
*/
public static SearchDocument from(Hit<?> hit, JsonpMapper jsonpMapper) {
Assert.notNull(hit, "hit must not be null");
Assert.notNull(hit, "hit must not be null");
Map<String, List<String>> highlightFields = hit.highlight();
Map<String, List<String>> highlightFields = hit.highlight();
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
hit.innerHits().forEach((name, innerHitsResult) -> {
// noinspection ReturnOfNull
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null,
searchDocument -> null, jsonpMapper));
});
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
hit.innerHits().forEach((name, innerHitsResult) -> {
// noinspection ReturnOfNull
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null,
searchDocument -> null, jsonpMapper));
});
NestedMetaData nestedMetaData = from(hit.nested());
NestedMetaData nestedMetaData = from(hit.nested());
Explanation explanation = from(hit.explanation());
Explanation explanation = from(hit.explanation());
Map<String, Double> matchedQueries = hit.matchedQueries();
Map<String, Double> matchedQueries = hit.matchedQueries();
Function<Map<String, JsonData>, EntityAsMap> fromFields = fields -> {
StringBuilder sb = new StringBuilder("{");
final boolean[] firstField = {true};
hit.fields().forEach((key, jsonData) -> {
if (!firstField[0]) {
sb.append(',');
}
sb.append('"').append(key).append("\":") //
.append(jsonData.toJson(jsonpMapper).toString());
firstField[0] = false;
});
sb.append('}');
return new EntityAsMap().fromJson(sb.toString());
};
EntityAsMap hitFieldsAsMap = new EntityAsMap();
hit.fields().forEach((key, jsonData) -> hitFieldsAsMap.put(key, toJavaObject(jsonData, jsonpMapper)));
EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields());
Map<String, List<@Nullable Object>> documentFields = new LinkedHashMap<>();
hitFieldsAsMap.forEach((key, value) -> {
if (value instanceof List) {
// noinspection unchecked
documentFields.put(key, (List<Object>) value);
} else {
documentFields.put(key, Collections.singletonList(value));
}
});
Map<String, List<Object>> documentFields = new LinkedHashMap<>();
hitFieldsAsMap.forEach((key, value) -> {
if (value instanceof List) {
// noinspection unchecked
documentFields.put(key, (List<Object>) value);
} else {
documentFields.put(key, Collections.singletonList(value));
}
});
Document document;
Object source = hit.source();
if (source == null) {
document = Document.from(hitFieldsAsMap);
} else {
if (source instanceof EntityAsMap entityAsMap) {
document = Document.from(entityAsMap);
} else if (source instanceof JsonData jsonData) {
document = Document.from(jsonData.to(EntityAsMap.class));
} else {
Document document;
Object source = hit.source();
if (source == null) {
document = Document.from(hitFieldsAsMap);
} else {
if (source instanceof EntityAsMap entityAsMap) {
document = Document.from(entityAsMap);
} else if (source instanceof JsonData jsonData) {
document = Document.from(jsonData.to(EntityAsMap.class));
} else {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName()));
}
document = Document.create();
}
}
document.setIndex(hit.index());
document.setId(hit.id());
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName()));
}
document = Document.create();
}
}
document.setIndex(hit.index());
document.setId(hit.id());
if (hit.version() != null) {
document.setVersion(hit.version());
}
document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); // -2 was the default value in the
// old client
document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0);
if (hit.version() != null) {
document.setVersion(hit.version());
}
document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); // -2 was the default value in the
// old client
document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0);
float score = hit.score() != null ? hit.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, hit.sort().stream().map(TypeUtils::toObject).toArray(),
documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
}
float score = hit.score() != null ? hit.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, hit.sort().stream().map(TypeUtils::toObject).toArray(),
documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
}
@Nullable
private static Object toJavaObject(JsonData jsonData, JsonpMapper jsonpMapper) {
return toJavaObject(jsonData.toJson(jsonpMapper));
}
public static SearchDocument from(CompletionSuggestOption<EntityAsMap> completionSuggestOption) {
@Nullable
private static Object toJavaObject(JsonValue jsonValue) {
Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source())
: Document.create();
document.setIndex(completionSuggestOption.index());
return switch (jsonValue.getValueType()) {
case OBJECT -> toMap(jsonValue.asJsonObject());
case ARRAY -> toList(jsonValue.asJsonArray());
case STRING -> ((JsonString) jsonValue).getString();
case NUMBER -> toNumber((JsonNumber) jsonValue);
case TRUE -> Boolean.TRUE;
case FALSE -> Boolean.FALSE;
case NULL -> null;
};
}
if (completionSuggestOption.id() != null) {
document.setId(completionSuggestOption.id());
}
private static Map<String, @Nullable Object> toMap(JsonObject jsonObject) {
float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, new Object[]{}, Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyMap(), null, null, null, completionSuggestOption.routing());
}
Map<String, @Nullable Object> result = new LinkedHashMap<>();
jsonObject.forEach((key, value) -> result.put(key, toJavaObject(value)));
return result;
}
@Nullable
private static Explanation from(co.elastic.clients.elasticsearch.core.explain.@Nullable Explanation explanation) {
private static List<@Nullable Object> toList(JsonArray jsonArray) {
if (explanation == null) {
return null;
}
List<Explanation> details = explanation.details().stream().map(DocumentAdapters::from).collect(Collectors.toList());
return new Explanation(true, (double) explanation.value(), explanation.description(), details);
}
List<@Nullable Object> result = new ArrayList<>(jsonArray.size());
jsonArray.forEach(value -> result.add(toJavaObject(value)));
return result;
}
private static Explanation from(ExplanationDetail explanationDetail) {
private static Number toNumber(JsonNumber jsonNumber) {
List<Explanation> details = explanationDetail.details().stream().map(DocumentAdapters::from)
.collect(Collectors.toList());
return new Explanation(null, (double) explanationDetail.value(), explanationDetail.description(), details);
}
if (!jsonNumber.isIntegral()) {
return jsonNumber.doubleValue();
}
@Nullable
private static NestedMetaData from(@Nullable NestedIdentity nestedIdentity) {
try {
return jsonNumber.intValueExact();
} catch (ArithmeticException ignored) {
// continue with a wider numeric type
}
if (nestedIdentity == null) {
return null;
}
try {
return jsonNumber.longValueExact();
} catch (ArithmeticException ignored) {
// continue with a wider numeric type
}
NestedMetaData child = from(nestedIdentity.nested());
return NestedMetaData.of(nestedIdentity.field(), nestedIdentity.offset(), child);
}
BigInteger value = jsonNumber.bigIntegerValue();
return value;
}
/**
* Creates a {@link Document} from a {@link GetResponse} where the found document is contained as {@link EntityAsMap}.
*
* @param getResponse the response instance
* @return the Document
*/
@Nullable
public static Document from(GetResult<EntityAsMap> getResponse) {
public static SearchDocument from(CompletionSuggestOption<EntityAsMap> completionSuggestOption) {
Assert.notNull(getResponse, "getResponse must not be null");
Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source())
: Document.create();
document.setIndex(completionSuggestOption.index());
if (!getResponse.found()) {
return null;
}
if (completionSuggestOption.id() != null) {
document.setId(completionSuggestOption.id());
}
Document document = getResponse.source() != null ? Document.from(getResponse.source()) : Document.create();
document.setIndex(getResponse.index());
document.setId(getResponse.id());
float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, new Object[] {}, Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyMap(), null, null, null, completionSuggestOption.routing());
}
if (getResponse.version() != null) {
document.setVersion(getResponse.version());
}
@Nullable
private static Explanation from(co.elastic.clients.elasticsearch.core.explain.@Nullable Explanation explanation) {
if (getResponse.seqNo() != null) {
document.setSeqNo(getResponse.seqNo());
}
if (explanation == null) {
return null;
}
List<Explanation> details = explanation.details().stream().map(DocumentAdapters::from).collect(Collectors.toList());
return new Explanation(true, (double) explanation.value(), explanation.description(), details);
}
if (getResponse.primaryTerm() != null) {
document.setPrimaryTerm(getResponse.primaryTerm());
}
private static Explanation from(ExplanationDetail explanationDetail) {
return document;
}
List<Explanation> details = explanationDetail.details().stream().map(DocumentAdapters::from)
.collect(Collectors.toList());
return new Explanation(null, (double) explanationDetail.value(), explanationDetail.description(), details);
}
/**
* Creates a list of {@link MultiGetItem}s from a {@link MgetResponse} where the data is contained as
* {@link EntityAsMap} instances.
*
* @param mgetResponse the response instance
* @return list of multiget items
*/
public static List<MultiGetItem<Document>> from(MgetResponse<EntityAsMap> mgetResponse) {
@Nullable
private static NestedMetaData from(@Nullable NestedIdentity nestedIdentity) {
Assert.notNull(mgetResponse, "mgetResponse must not be null");
if (nestedIdentity == null) {
return null;
}
return mgetResponse.docs().stream() //
.map(itemResponse -> MultiGetItem.of( //
itemResponse.isFailure() ? null : from(itemResponse.result()), //
ResponseConverter.getFailure(itemResponse)))
.collect(Collectors.toList());
}
NestedMetaData child = from(nestedIdentity.nested());
return NestedMetaData.of(nestedIdentity.field(), nestedIdentity.offset(), child);
}
/**
* Creates a {@link Document} from a {@link GetResponse} where the found document is contained as {@link EntityAsMap}.
*
* @param getResponse the response instance
* @return the Document
*/
@Nullable
public static Document from(GetResult<EntityAsMap> getResponse) {
Assert.notNull(getResponse, "getResponse must not be null");
if (!getResponse.found()) {
return null;
}
Document document = getResponse.source() != null ? Document.from(getResponse.source()) : Document.create();
document.setIndex(getResponse.index());
document.setId(getResponse.id());
if (getResponse.version() != null) {
document.setVersion(getResponse.version());
}
if (getResponse.seqNo() != null) {
document.setSeqNo(getResponse.seqNo());
}
if (getResponse.primaryTerm() != null) {
document.setPrimaryTerm(getResponse.primaryTerm());
}
return document;
}
/**
* Creates a list of {@link MultiGetItem}s from a {@link MgetResponse} where the data is contained as
* {@link EntityAsMap} instances.
*
* @param mgetResponse the response instance
* @return list of multiget items
*/
public static List<MultiGetItem<Document>> from(MgetResponse<EntityAsMap> mgetResponse) {
Assert.notNull(mgetResponse, "mgetResponse must not be null");
return mgetResponse.docs().stream() //
.map(itemResponse -> MultiGetItem.of( //
itemResponse.isFailure() ? null : from(itemResponse.result()), //
ResponseConverter.getFailure(itemResponse)))
.collect(Collectors.toList());
}
}
@@ -114,7 +114,7 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
}
@Override
public boolean create(Map<String, Object> settings) {
public boolean create(Map<String, @Nullable Object> settings) {
Assert.notNull(settings, "settings must not be null");
@@ -122,7 +122,7 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
}
@Override
public boolean create(Map<String, Object> settings, Document mapping) {
public boolean create(Map<String, @Nullable Object> settings, Document mapping) {
Assert.notNull(settings, "settings must not be null");
Assert.notNull(mapping, "mapping must not be null");
@@ -135,7 +135,7 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
return doCreate(getIndexCoordinates(), createSettings(), createMapping());
}
protected boolean doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
protected boolean doCreate(IndexCoordinates indexCoordinates, Map<String, @Nullable Object> settings,
@Nullable Document mapping) {
Set<Alias> aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>();
CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates)
@@ -233,7 +233,7 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
}
@Override
public Map<String, Object> getMapping() {
public Map<String, @Nullable Object> getMapping() {
IndexCoordinates indexCoordinates = getIndexCoordinates();
GetMappingRequest getMappingRequest = requestConverter.indicesGetMappingRequest(indexCoordinates);
@@ -18,9 +18,30 @@ 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 io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
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;
@@ -52,28 +73,6 @@ 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.
@@ -269,7 +268,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
ExistsRequest existsRequest = requestConverter.documentExistsRequest(id, routingResolver.getRouting(), index);
return Mono.from(execute(
((ClientCallback<@NonNull Publisher<BooleanResponse>>) client -> client.exists(existsRequest))))
((ClientCallback<Publisher<BooleanResponse>>) client -> client.exists(existsRequest))))
.map(BooleanResponse::value) //
.onErrorReturn(NoSuchIndexException.class, false);
}
@@ -381,7 +380,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
for (BulkResponseItem item : bulkResponse.items()) {
if (item.error() != null) {
if (item.error() != null && item.id() != null) {
failedDocuments.put(item.id(), new BulkFailureException.FailureDetails(item.status(), item.error().reason()));
}
}
@@ -538,7 +537,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
return Mono.empty();
}
List<Object> sortOptions = hits.get(hits.size() - 1).sort().stream().map(TypeUtils::toObject)
List<Object> sortOptions = hits.get(hits.size() - 1).sort().stream().map(TypeUtils::toObjectNotNull)
.collect(Collectors.toList());
baseQuery.setSearchAfter(sortOptions);
SearchRequest followSearchRequest = requestConverter.searchRequest(baseQuery,
@@ -632,8 +631,8 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
SearchRequest searchRequest = requestConverter.searchRequest(query, routingResolver.getRouting(), clazz, index,
false);
// noinspection unchecked
SearchDocumentCallback<T> callback = new ReadSearchDocumentCallback<>((Class<T>) clazz, index);
SearchDocumentResponse.EntityCreator<T> entityCreator = searchDocument -> callback.toEntity(searchDocument)
.toFuture();
@@ -108,7 +108,7 @@ public class ReactiveIndicesTemplate
}
@Override
public Mono<Boolean> create(Map<String, Object> settings) {
public Mono<Boolean> create(Map<String, @Nullable Object> settings) {
Assert.notNull(settings, "settings must not be null");
@@ -116,7 +116,7 @@ public class ReactiveIndicesTemplate
}
@Override
public Mono<Boolean> create(Map<String, Object> settings, Document mapping) {
public Mono<Boolean> create(Map<String, @Nullable Object> settings, Document mapping) {
Assert.notNull(settings, "settings must not be null");
Assert.notNull(mapping, "mapping must not be null");
@@ -132,7 +132,7 @@ public class ReactiveIndicesTemplate
doCreate(getIndexCoordinates(), settings, mapping))); //
}
private Mono<Boolean> doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
private Mono<Boolean> doCreate(IndexCoordinates indexCoordinates, Map<String, @Nullable Object> settings,
@Nullable Document mapping) {
Set<Alias> aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>();
CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates)
@@ -270,7 +270,7 @@ public class ReactiveIndicesTemplate
return getAliases(null, indexNames);
}
private Mono<Map<String, Set<AliasData>>> getAliases(@Nullable String[] aliasNames, @Nullable String[] indexNames) {
private Mono<Map<String, Set<AliasData>>> getAliases(String@Nullable [] aliasNames, String@Nullable [] indexNames) {
GetAliasRequest getAliasRequest = requestConverter.indicesGetAliasRequest(aliasNames, indexNames);
Mono<GetAliasResponse> getAliasResponse = Mono.from(execute(client -> client.getAlias(getAliasRequest)));
@@ -74,6 +74,7 @@ import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -113,6 +114,7 @@ import org.springframework.util.StringUtils;
* @author cdalxndr
* @author scoobyzhang
* @author Haibo Liu
* @author Steven Pearce
* @since 4.4
*/
class RequestConverter extends AbstractQueryProcessor {
@@ -173,17 +175,9 @@ class RequestConverter extends AbstractQueryProcessor {
private co.elastic.clients.elasticsearch.indices.Alias.Builder buildAlias(AliasActionParameters parameters,
co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder) {
if (parameters.getRouting() != null) {
aliasBuilder.routing(parameters.getRouting());
}
if (parameters.getIndexRouting() != null) {
aliasBuilder.indexRouting(parameters.getIndexRouting());
}
if (parameters.getSearchRouting() != null) {
aliasBuilder.searchRouting(parameters.getSearchRouting());
}
getRouting(parameters.getRouting()).ifPresent(aliasBuilder::routing);
getRouting(parameters.getIndexRouting()).ifPresent(aliasBuilder::indexRouting);
getRouting(parameters.getSearchRouting()).ifPresent(aliasBuilder::searchRouting);
if (parameters.getHidden() != null) {
aliasBuilder.isHidden(parameters.getHidden());
@@ -239,12 +233,16 @@ class RequestConverter extends AbstractQueryProcessor {
Map<String, co.elastic.clients.elasticsearch.indices.Alias> aliases = new HashMap<>();
for (Alias alias : indexSettings.getAliases()) {
co.elastic.clients.elasticsearch.indices.Alias esAlias = co.elastic.clients.elasticsearch.indices.Alias
.of(ab -> ab.filter(getQuery(alias.getFilter(), null))
.routing(alias.getRouting())
.indexRouting(alias.getIndexRouting())
.searchRouting(alias.getSearchRouting())
.isHidden(alias.getHidden())
.isWriteIndex(alias.getWriteIndex()));
.of(ab -> {
co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder = ab.filter(getQuery(alias.getFilter(), null))
.isHidden(alias.getHidden())
.isWriteIndex(alias.getWriteIndex());
getRouting(alias.getRouting()).ifPresent(aliasBuilder::routing);
getRouting(alias.getIndexRouting()).ifPresent(aliasBuilder::indexRouting);
getRouting(alias.getSearchRouting()).ifPresent(aliasBuilder::searchRouting);
return aliasBuilder;
});
aliases.put(alias.getAlias(), esAlias);
}
@@ -318,10 +316,11 @@ class RequestConverter extends AbstractQueryProcessor {
addActionBuilder //
.indices(Arrays.asList(parameters.getIndices())) //
.isHidden(parameters.getHidden()) //
.isWriteIndex(parameters.getWriteIndex()) //
.routing(parameters.getRouting()) //
.indexRouting(parameters.getIndexRouting()) //
.searchRouting(parameters.getSearchRouting()); //
.isWriteIndex(parameters.getWriteIndex()); //
getRouting(parameters.getRouting()).ifPresent(addActionBuilder::routing);
getRouting(parameters.getIndexRouting()).ifPresent(addActionBuilder::indexRouting);
getRouting(parameters.getSearchRouting()).ifPresent(addActionBuilder::searchRouting);
if (parameters.getAliases() != null) {
addActionBuilder.aliases(Arrays.asList(parameters.getAliases()));
@@ -589,10 +588,11 @@ class RequestConverter extends AbstractQueryProcessor {
builder.version(query.getVersion()).versionType(versionType);
}
builder //
.ifSeqNo(query.getSeqNo()) //
.ifPrimaryTerm(query.getPrimaryTerm()) //
.routing(query.getRouting()); //
builder
.ifSeqNo(query.getSeqNo())
.ifPrimaryTerm(query.getPrimaryTerm());
getRouting(query.getRouting()).ifPresent(builder::routing);
if (query.getOpType() != null) {
switch (query.getOpType()) {
@@ -642,8 +642,9 @@ class RequestConverter extends AbstractQueryProcessor {
builder //
.ifSeqNo(query.getSeqNo()) //
.ifPrimaryTerm(query.getPrimaryTerm()) //
.routing(query.getRouting()); //
.ifPrimaryTerm(query.getPrimaryTerm()); //
getRouting(query.getRouting()).ifPresent(builder::routing);
return builder.build();
}
@@ -684,8 +685,9 @@ class RequestConverter extends AbstractQueryProcessor {
builder //
.ifSeqNo(query.getSeqNo()) //
.ifPrimaryTerm(query.getPrimaryTerm()) //
.routing(query.getRouting()); //
.ifPrimaryTerm(query.getPrimaryTerm()); //
getRouting(query.getRouting()).ifPresent(builder::routing);
return builder.build();
}
@@ -722,11 +724,12 @@ class RequestConverter extends AbstractQueryProcessor {
});
uob
.routing(query.getRouting())
.ifSeqNo(query.getIfSeqNo())
.ifPrimaryTerm(query.getIfPrimaryTerm())
.retryOnConflict(query.getRetryOnConflict());
getRouting(query.getRouting()).ifPresent(uob::routing);
// no refresh, timeout, waitForActiveShards on UpdateOperation or UpdateAction
return uob.build();
@@ -776,9 +779,7 @@ class RequestConverter extends AbstractQueryProcessor {
builder.pipeline(bulkOptions.getPipeline());
}
if (bulkOptions.getRoutingId() != null) {
builder.routing(bulkOptions.getRoutingId());
}
getRouting(bulkOptions.getRoutingId()).ifPresent(builder::routing);
List<BulkOperation> operations = queries.stream().map(query -> {
BulkOperation.Builder ob = new BulkOperation.Builder();
@@ -805,10 +806,13 @@ class RequestConverter extends AbstractQueryProcessor {
Assert.notNull(id, "id must not be null");
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
return GetRequest.of(grb -> grb //
.index(indexCoordinates.getIndexName()) //
.id(id) //
.routing(routing));
return GetRequest.of(grb -> {
GetRequest.Builder builder = grb //
.index(indexCoordinates.getIndexName()) //
.id(id); //
getRouting(routing).ifPresent(builder::routing);
return builder;
});
}
public co.elastic.clients.elasticsearch.core.ExistsRequest documentExistsRequest(String id, @Nullable String routing,
@@ -817,10 +821,13 @@ class RequestConverter extends AbstractQueryProcessor {
Assert.notNull(id, "id must not be null");
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
return co.elastic.clients.elasticsearch.core.ExistsRequest.of(erb -> erb
.index(indexCoordinates.getIndexName())
.id(id)
.routing(routing));
return co.elastic.clients.elasticsearch.core.ExistsRequest.of(erb -> {
co.elastic.clients.elasticsearch.core.ExistsRequest.Builder builder = erb
.index(indexCoordinates.getIndexName())
.id(id);
getRouting(routing).ifPresent(builder::routing);
return builder;
});
}
public <T> MgetRequest documentMgetRequest(Query query, Class<T> clazz, IndexCoordinates index) {
@@ -838,11 +845,14 @@ class RequestConverter extends AbstractQueryProcessor {
SourceConfig sourceConfig = getSourceConfig(query);
List<MultiGetOperation> multiGetOperations = query.getIdsWithRouting().stream()
.map(idWithRouting -> MultiGetOperation.of(mgo -> mgo //
.index(index.getIndexName()) //
.id(idWithRouting.id()) //
.routing(idWithRouting.routing()) //
.source(sourceConfig)))
.map(idWithRouting -> MultiGetOperation.of(mgo -> {
MultiGetOperation.Builder builder = mgo //
.index(index.getIndexName()) //
.id(idWithRouting.id()) //
.source(sourceConfig);
getRouting(idWithRouting.routing()).ifPresent(builder::routing);
return builder;
}))
.collect(Collectors.toList());
return MgetRequest.of(mg -> mg//
@@ -964,10 +974,7 @@ class RequestConverter extends AbstractQueryProcessor {
return DeleteRequest.of(r -> {
r.id(id).index(index.getIndexName());
if (routing != null) {
r.routing(routing);
}
getRouting(routing).ifPresent(r::routing);
r.refresh(refresh(refreshPolicy));
return r;
});
@@ -991,11 +998,7 @@ class RequestConverter extends AbstractQueryProcessor {
b.scroll(time(query.getScrollTime()));
if (query.getRoute() != null) {
b.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
b.routing(routing);
}
getRouting(query.getRoute(), routing).ifPresent(b::routing);
return b;
});
@@ -1015,11 +1018,7 @@ class RequestConverter extends AbstractQueryProcessor {
.scroll(time(query.getScroll()))
.scrollSize(query.getScrollSize());
if (query.getRouting() != null) {
dqb.routing(query.getRouting());
} else if (StringUtils.hasText(routing)) {
dqb.routing(routing);
}
getRouting(query.getRouting(), routing).ifPresent(dqb::routing);
if (query.getQ() != null) {
dqb.q(query.getQ())
@@ -1099,7 +1098,6 @@ class RequestConverter extends AbstractQueryProcessor {
uqb
.doc(query.getDocument())
.upsert(query.getUpsert())
.routing(query.getRouting() != null ? query.getRouting() : routing)
.scriptedUpsert(query.getScriptedUpsert())
.docAsUpsert(query.getDocAsUpsert())
.ifSeqNo(query.getIfSeqNo())
@@ -1107,6 +1105,8 @@ class RequestConverter extends AbstractQueryProcessor {
.refresh(query.getRefreshPolicy() != null ? refresh(query.getRefreshPolicy()) : refresh(refreshPolicy))
.retryOnConflict(query.getRetryOnConflict());
getRouting(query.getRouting(), routing).ifPresent(uqb::routing);
if (query.getFetchSource() != null) {
uqb.source(sc -> sc.fetch(query.getFetchSource()));
}
@@ -1149,13 +1149,14 @@ class RequestConverter extends AbstractQueryProcessor {
ub //
.index(Arrays.asList(index.getIndexNames())) //
.refresh(refreshPolicy == RefreshPolicy.IMMEDIATE) //
.routing(updateQuery.getRouting()) //
.script(getScript(updateQuery.getScriptData())) //
.maxDocs(updateQuery.getMaxDocs() != null ? Long.valueOf(updateQuery.getMaxDocs()) : null) //
.pipeline(updateQuery.getPipeline()) //
.requestsPerSecond(updateQuery.getRequestsPerSecond()) //
.slices(slices(updateQuery.getSlices() != null ? Long.valueOf(updateQuery.getSlices()) : null));
getRouting(updateQuery.getRouting()).ifPresent(ub::routing);
if (updateQuery.getAbortOnVersionConflict() != null) {
ub.conflicts(updateQuery.getAbortOnVersionConflict() ? Conflicts.Abort : Conflicts.Proceed);
}
@@ -1229,12 +1230,7 @@ class RequestConverter extends AbstractQueryProcessor {
builder.query(getQuery(query, clazz));
if (StringUtils.hasText(query.getRoute())) {
builder.routing(query.getRoute());
}
if (StringUtils.hasText(routing)) {
builder.routing(routing);
}
getRouting(query.getRoute(), routing).ifPresent(builder::routing);
addPostFilter(query, builder);
@@ -1401,11 +1397,7 @@ class RequestConverter extends AbstractQueryProcessor {
.requestCache(query.getRequestCache()) //
;
if (StringUtils.hasText(query.getRoute())) {
h.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
h.routing(routing);
}
getRouting(query.getRoute(), routing).ifPresent(h::routing);
if (query.getPreference() != null) {
h.preference(query.getPreference());
@@ -1451,11 +1443,7 @@ class RequestConverter extends AbstractQueryProcessor {
builder.expandWildcards(expandWildcards(expandWildcards));
}
if (query.getRoute() != null) {
builder.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
builder.routing(routing);
}
getRouting(query.getRoute(), routing).ifPresent(builder::routing);
if (query.getPreference() != null) {
builder.preference(query.getPreference());
@@ -1886,11 +1874,7 @@ class RequestConverter extends AbstractQueryProcessor {
if (query.getSource() != null) {
builder.source(so -> so.scriptString(query.getSource()));
}
if (query.getRoute() != null) {
builder.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
builder.routing(routing);
}
getRouting(query.getRoute(), routing).ifPresent(builder::routing);
var expandWildcards = query.getExpandWildcards();
if (expandWildcards != null && !expandWildcards.isEmpty()) {
@@ -1990,6 +1974,16 @@ class RequestConverter extends AbstractQueryProcessor {
return null;
}
Optional<String> getRouting(@Nullable String routing) {
if (StringUtils.hasText(routing)) {
return Optional.of(routing);
}
return Optional.empty();
}
Optional<String> getRouting(@Nullable String routing1, @Nullable String routing2) {
return getRouting(routing1).or(() -> getRouting(routing2));
}
private VersionType retrieveVersionTypeFromPersistentEntity(@Nullable Class<?> clazz) {
ElasticsearchPersistentEntity<?> persistentEntity = getPersistentEntity(clazz);
@@ -22,7 +22,7 @@ import co.elastic.clients.elasticsearch._types.BulkIndexByScrollFailure;
import co.elastic.clients.elasticsearch._types.ErrorCause;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.cluster.ComponentTemplateSummary;
import co.elastic.clients.elasticsearch.cluster.ComponentTemplateSummaryRes;
import co.elastic.clients.elasticsearch.cluster.GetComponentTemplateResponse;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse;
@@ -66,6 +66,7 @@ import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.core.sql.SqlResponse;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
/**
@@ -132,7 +133,7 @@ class ResponseConverter {
.build();
}
private TemplateResponseData clusterGetComponentTemplateData(ComponentTemplateSummary componentTemplateSummary) {
private TemplateResponseData clusterGetComponentTemplateData(ComponentTemplateSummaryRes componentTemplateSummary) {
var mapping = typeMapping(componentTemplateSummary.mappings());
var settings = new Settings();
@@ -335,11 +336,11 @@ class ResponseConverter {
.build();
}
private TemplateResponseData indexGetComponentTemplateData(IndexTemplateSummary indexTemplateSummary,
private TemplateResponseData indexGetComponentTemplateData(IndexTemplateSummaryWithRollover indexTemplateSummary,
List<String> composedOf) {
var mapping = typeMapping(indexTemplateSummary.mappings());
Function<IndexSettings, Settings> indexSettingsToSettings = indexSettings -> {
Function<@Nullable IndexSettings, @Nullable Settings> indexSettingsToSettings = indexSettings -> {
if (indexSettings == null) {
return null;
@@ -497,7 +498,7 @@ class ResponseConverter {
builder.withDeleted(response.deleted());
}
if(response.updated() != null) {
if (response.updated() != null) {
builder.withUpdated(response.updated());
}
@@ -574,17 +575,20 @@ class ResponseConverter {
}
}
@Contract("null -> null; !null -> !null")
@Nullable
static ElasticsearchErrorCause toErrorCause(@Nullable ErrorCause errorCause) {
if (errorCause != null) {
return new ElasticsearchErrorCause( //
errorCause.type(), //
errorCause.reason(), //
errorCause.stackTrace(), //
toErrorCause(errorCause.causedBy()), //
errorCause.rootCause().stream().map(ResponseConverter::toErrorCause).collect(Collectors.toList()), //
errorCause.suppressed().stream().map(ResponseConverter::toErrorCause).collect(Collectors.toList()));
return new ElasticsearchErrorCause(
errorCause.type(),
errorCause.reason(),
errorCause.stackTrace(),
toErrorCause(errorCause.causedBy()),
(List<ElasticsearchErrorCause>) (errorCause.rootCause().stream()
.map(ResponseConverter::toErrorCause).collect(Collectors.toList())),
(List<ElasticsearchErrorCause>) (errorCause.suppressed().stream().map(ResponseConverter::toErrorCause)
.collect(Collectors.toList())));
} else {
return null;
}
@@ -36,6 +36,7 @@ import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
@@ -144,6 +145,12 @@ final class TypeUtils {
}
}
static Object toObjectNotNull(FieldValue fieldValue) {
Objects.requireNonNull(fieldValue);
return toObject(fieldValue);
}
@Nullable
static Object toObject(@Nullable FieldValue fieldValue) {
@@ -485,7 +492,7 @@ final class TypeUtils {
}
@Nullable
static IndexSettings indexSettings(@Nullable Map<String, Object> settings) {
static IndexSettings indexSettings(@Nullable Map<String, @Nullable Object> settings) {
return settings != null ? IndexSettings.of(b -> b.withJson(new StringReader(Document.from(settings).toJson())))
: null;
}
@@ -15,15 +15,9 @@
*/
package org.springframework.data.elasticsearch.client.elc.aot;
import co.elastic.clients.elasticsearch._types.mapping.RuntimeFieldType;
import co.elastic.clients.elasticsearch._types.mapping.TypeMapping;
import co.elastic.clients.elasticsearch.indices.IndexSettings;
import co.elastic.clients.elasticsearch.indices.PutMappingRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.util.ClassUtils;
/**
@@ -38,10 +32,14 @@ public class ElasticsearchClientRuntimeHints implements RuntimeHintsRegistrar {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.reflection()
.registerType(TypeReference.of(IndexSettings.class), builder -> builder.withField("_DESERIALIZER"))
.registerType(TypeReference.of(PutMappingRequest.class), builder -> builder.withField("_DESERIALIZER"))
.registerType(TypeReference.of(RuntimeFieldType.class), builder -> builder.withField("_DESERIALIZER"))
.registerType(TypeReference.of(TypeMapping.class), builder -> builder.withField("_DESERIALIZER"));
.registerTypeIfPresent(classLoader, "co.elastic.clients.elasticsearch.indices.IndexSettings",
builder -> builder.withField("_DESERIALIZER"))
.registerTypeIfPresent(classLoader, "co.elastic.clients.elasticsearch.indices.PutMappingRequest",
builder -> builder.withField("_DESERIALIZER"))
.registerTypeIfPresent(classLoader, "co.elastic.clients.elasticsearch._types.mapping.RuntimeFieldType",
builder -> builder.withField("_DESERIALIZER"))
.registerTypeIfPresent(classLoader, "co.elastic.clients.elasticsearch._types.mapping.TypeMapping",
builder -> builder.withField("_DESERIALIZER"));
if (ClassUtils.isPresent("org.apache.http.impl.auth.BasicScheme",
ElasticsearchClientRuntimeHints.class.getClassLoader())) {
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.client.elc.aot;
@@ -28,7 +28,6 @@ import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
import org.apache.hc.core5.util.Timeout;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.support.HttpHeaders;
@@ -94,16 +93,18 @@ public final class Rest5Clients {
throw new RuntimeException(e);
}
}
httpAsyncClientBuilder.addRequestInterceptorFirst((request, entity, context) -> {
clientConfiguration.getHeadersSupplier().get().forEach((header, values) -> {
// The accept and content-type headers are already put on the request, despite this being the first
// interceptor.
if ("Accept".equalsIgnoreCase(header) || " Content-Type".equalsIgnoreCase(header)) {
request.removeHeaders(header);
}
values.forEach(value -> request.addHeader(header, value));
});
});
httpAsyncClientBuilder.addExecInterceptorFirst("es-rest5-client",
(request, entityProducer, scope, chain, asyncExecCallback) -> {
clientConfiguration.getHeadersSupplier().get().forEach((header, values) -> {
// The accept and content-type headers may already be put on the request, despite this being the
// first interceptor.
if ("Accept".equalsIgnoreCase(header) || "Content-Type".equalsIgnoreCase(header)) {
request.removeHeaders(header);
}
values.forEach(value -> request.addHeader(header, value));
});
chain.proceed(request, entityProducer, scope, asyncExecCallback);
});
// add httpclient configurator callbacks provided by the configuration
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
@@ -181,7 +182,7 @@ public final class Rest5Clients {
return builder;
}
private static HttpHost @NonNull [] getHttpHosts(ClientConfiguration clientConfiguration) {
private static HttpHost[] getHttpHosts(ClientConfiguration clientConfiguration) {
List<InetSocketAddress> hosts = clientConfiguration.getEndpoints();
boolean useSsl = clientConfiguration.useSsl();
return hosts.stream()
@@ -21,10 +21,8 @@ import org.apache.http.protocol.HttpContext;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients;
import org.springframework.data.elasticsearch.support.HttpHeaders;
import org.springframework.util.Assert;
@@ -107,7 +105,7 @@ public final class RestClients {
return builder;
}
private static HttpHost @NonNull [] getHttpHosts(ClientConfiguration clientConfiguration) {
private static HttpHost[] getHttpHosts(ClientConfiguration clientConfiguration) {
List<InetSocketAddress> hosts = clientConfiguration.getEndpoints();
boolean useSsl = clientConfiguration.useSsl();
return hosts.stream()
@@ -130,10 +128,10 @@ public final class RestClients {
record CustomHeaderInjector(Supplier<HttpHeaders> headersSupplier) implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) {
public void process(@Nullable HttpRequest request, @Nullable HttpContext context) {
HttpHeaders httpHeaders = headersSupplier.get();
if (httpHeaders != null && !httpHeaders.isEmpty()) {
if (!httpHeaders.isEmpty() && request != null) {
Arrays.stream(toHeaderArray(httpHeaders)).forEach(request::addHeader);
}
}
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.client;
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.client.util;
@@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.config;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
@@ -41,6 +42,7 @@ import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Steven Pearce
* @since 3.2
*/
@Configuration(proxyBeanMethods = false)
@@ -72,6 +74,7 @@ public class ElasticsearchConfigurationSupport {
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
mappingContext.setWriteTypeHints(writeTypeHints());
mappingContext.setServerType(serverType());
return mappingContext;
}
@@ -98,7 +101,7 @@ public class ElasticsearchConfigurationSupport {
protected Collection<String> getMappingBasePackages() {
Package mappingBasePackage = getClass().getPackage();
return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
return mappingBasePackage == null ? Collections.emptyList() : List.of(mappingBasePackage.getName());
}
/**
@@ -173,7 +176,7 @@ public class ElasticsearchConfigurationSupport {
}
/**
* Flag specifiying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
* Flag specifying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
* default value of {@literal true}. If you need to write to an existing index that does not have a mapping defined
* for these fields and that has a strict mapping set, then it might be necessary to disable type hints. But notice
* that in this case reading polymorphic types may fail.
@@ -184,4 +187,15 @@ public class ElasticsearchConfigurationSupport {
protected boolean writeTypeHints() {
return true;
}
/**
* Configures the {@link ElasticsearchServerType} to use when creating indexes. The Default value will support the
* standard configurations, and SERVERLESS will support ElasticSearch Serverless
*
* @return the {@link ElasticsearchServerType} to use
* @since 6.2
*/
protected ElasticsearchServerType serverType() {
return ElasticsearchServerType.DEFAULT;
}
}
@@ -0,0 +1,37 @@
/*
* 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.config;
/**
* ElasticsearchServerType defines the type of Elasticsearch server you are connecting to.
* @see #DEFAULT
* @see #SERVERLESS
*
* @author Steven Pearce
* @since 6.2
*/
public enum ElasticsearchServerType {
/**
* Normal installations of Elasticsearch including cloud-hosted Elasticsearch
*/
DEFAULT,
/**
* New Flavour of Elasticsearch, offered by Elastic
*/
SERVERLESS,
}
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.config;
@@ -59,6 +59,7 @@ import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -260,7 +261,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
List<IndexedObjectInformation> indexedObjectInformationList = bulkIndex(indexQueries, index);
Iterator<IndexedObjectInformation> iterator = indexedObjectInformationList.iterator();
// noinspection unchecked
// noinspection unchecked,DataFlowIssue
return indexQueries.stream() //
.map(IndexQuery::getObject) //
.map(entity -> (T) entityOperations.updateIndexedObject(
@@ -593,7 +594,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
}
protected <T> SearchDocumentResponse.EntityCreator<T> getEntityCreator(ReadDocumentCallback<T> documentCallback) {
return searchDocument -> CompletableFuture.completedFuture(documentCallback.doWith(searchDocument));
return searchDocument -> CompletableFuture.<T> completedFuture(documentCallback.doWith(searchDocument));
}
/**
@@ -752,6 +753,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
// region Document callbacks
protected interface DocumentCallback<T> {
@Contract("null -> null")
@Nullable
T doWith(@Nullable Document document);
}
@@ -815,6 +817,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
@Override
public SearchHits<T> doWith(SearchDocumentResponse response) {
// noinspection NullableProblems,DataFlowIssue
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
return SearchHitMapping.mappingFor(type, elasticsearchConverter).mapHits(response, entities);
}
@@ -835,6 +838,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
@Override
public SearchScrollHits<T> doWith(SearchDocumentResponse response) {
// noinspection DataFlowIssue,NullableProblems
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
return SearchHitMapping.mappingFor(type, elasticsearchConverter).mapScrollHits(response, entities);
}
@@ -766,7 +766,8 @@ abstract public class AbstractReactiveElasticsearchTemplate
/**
* Value class to capture client independent information from a response to an index request.
*/
public record IndexResponseMetaData(String id, String index, long seqNo, long primaryTerm, long version) {
public record IndexResponseMetaData(String id, String index, @Nullable Long seqNo, @Nullable Long primaryTerm,
long version) {
}
// endregion
@@ -17,7 +17,6 @@ package org.springframework.data.elasticsearch.core;
import java.util.Map;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
@@ -370,7 +369,7 @@ public class EntityOperations {
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#initializeVersionProperty()
*/
@Override
public @NonNull T initializeVersionProperty() {
public T initializeVersionProperty() {
return map;
}
@@ -399,7 +398,7 @@ public class EntityOperations {
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#incrementVersion()
*/
@Override
public @NonNull T incrementVersion() {
public T incrementVersion() {
return map;
}
@@ -408,7 +407,7 @@ public class EntityOperations {
* @see org.springframework.data.elasticsearch.core.EntityOperations.Entity#getBean()
*/
@Override
public @NonNull T getBean() {
public T getBean() {
return map;
}
@@ -52,7 +52,7 @@ public interface IndexOperations {
* @param settings the index settings
* @return {@literal true} if the index was created
*/
boolean create(Map<String, Object> settings);
boolean create(Map<String, @Nullable Object> settings);
/**
* Create an index for given settings and mapping.
@@ -62,7 +62,7 @@ public interface IndexOperations {
* @return {@literal true} if the index was created
* @since 4.2
*/
boolean create(Map<String, Object> settings, Document mapping);
boolean create(Map<String, @Nullable Object> settings, Document mapping);
/**
* Create an index with the settings and mapping defined for the entity this IndexOperations is bound to.
@@ -142,7 +142,7 @@ public interface IndexOperations {
*
* @return the mapping
*/
Map<String, Object> getMapping();
Map<String, @Nullable Object> getMapping();
// endregion
@@ -46,12 +46,12 @@ public interface IndexOperationsAdapter extends IndexOperations {
}
@Override
public boolean create(Map<String, Object> settings) {
public boolean create(Map<String, @Nullable Object> settings) {
return Boolean.TRUE.equals(reactiveIndexOperations.create(settings).block());
}
@Override
public boolean create(Map<String, Object> settings, Document mapping) {
public boolean create(Map<String, @Nullable Object> settings, Document mapping) {
return Boolean.TRUE.equals(reactiveIndexOperations.create(settings, mapping).block());
}
@@ -92,7 +92,7 @@ public interface IndexOperationsAdapter extends IndexOperations {
}
@Override
public Map<String, Object> getMapping() {
public Map<String, @Nullable Object> getMapping() {
return Objects.requireNonNull(reactiveIndexOperations.getMapping().block());
}
@@ -36,7 +36,7 @@ public class MultiGetItem<T> {
}
public static <T> MultiGetItem<T> of(@Nullable T item, @Nullable Failure failure) {
return new MultiGetItem<>(item, failure);
return new MultiGetItem(item, failure);
}
public boolean hasItem() {
@@ -21,6 +21,7 @@ import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.index.*;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
@@ -50,7 +51,7 @@ public interface ReactiveIndexOperations {
* @return a {@link Mono} signalling successful operation completion or an {@link Mono#error(Throwable) error} if eg.
* the index already exist.
*/
Mono<Boolean> create(Map<String, Object> settings);
Mono<Boolean> create(Map<String, @Nullable Object> settings);
/**
* Create an index for given settings and mapping.
@@ -61,7 +62,7 @@ public interface ReactiveIndexOperations {
* the index already exist.
* @since 4.2
*/
Mono<Boolean> create(Map<String, Object> settings, Document mapping);
Mono<Boolean> create(Map<String, @Nullable Object> settings, Document mapping);
/**
* Create an index with the settings and mapping defined for the entity this IndexOperations is bound to.
@@ -51,7 +51,7 @@ public class SearchHit<T> {
private final Map<String, Double> matchedQueries = new LinkedHashMap<>();
public SearchHit(@Nullable String index, @Nullable String id, @Nullable String routing, float score,
@Nullable Object[] sortValues, @Nullable Map<String, List<String>> highlightFields,
Object @Nullable [] sortValues, @Nullable Map<String, List<String>> highlightFields,
@Nullable Map<String, SearchHits<?>> innerHits, @Nullable NestedMetaData nestedMetaData,
@Nullable Explanation explanation, @Nullable Map<String, Double> matchedQueries, T content) {
this.index = index;
@@ -193,7 +193,6 @@ public class SearchHit<T> {
/**
* @return the matched queries for this SearchHit.
*/
@Nullable
public Map<String, Double> getMatchedQueries() {
return matchedQueries;
}
@@ -22,11 +22,11 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.data.core.ReactiveWrappers;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.util.CloseableIterator;
import org.springframework.lang.Contract;
/**
* Utility class with helper methods for working with {@link SearchHit}.
@@ -47,6 +47,7 @@ public final class SearchHitSupport {
* @return a corresponding object where the SearchHits are replaced by their content if possible, otherwise the
* original object
*/
@Contract("null -> null; !null -> !null")
@Nullable
public static Object unwrapSearchHits(@Nullable Object result) {
@@ -133,7 +133,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
}
@Override
public SearchShardStatistics getSearchShardStatistics() {
public @Nullable SearchShardStatistics getSearchShardStatistics() {
return searchShardStatistics;
}
@@ -1,5 +1,18 @@
/**
* Interfaces and classes related to Elasticsearch cluster information and management.
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.core.cluster;
@@ -29,7 +29,7 @@ import org.springframework.data.mapping.context.MappingContext;
* @author Christoph Strobl
* @since 3.2
*/
public interface ElasticsearchTypeMapper extends TypeMapper<Map<String, Object>> {
public interface ElasticsearchTypeMapper extends TypeMapper<Map<String, @Nullable Object>> {
String DEFAULT_TYPE_KEY = "_class";
@@ -47,7 +47,7 @@ public interface ElasticsearchTypeMapper extends TypeMapper<Map<String, Object>>
@Nullable
String getTypeKey();
default boolean containsTypeInformation(Map<String, Object> source) {
default boolean containsTypeInformation(Map<String, @Nullable Object> source) {
return readType(source) != null;
}
@@ -20,9 +20,9 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
@@ -167,7 +167,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJson<? extends Iterable<?>> convert(Map<String, Object> source) {
public GeoJson<? extends Iterable<?>> convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
@@ -206,7 +206,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJsonPoint convert(Map<String, Object> source) {
public GeoJsonPoint convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonPoint.TYPE), "does not contain a type 'Point'");
@@ -244,7 +244,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJsonMultiPoint convert(Map<String, Object> source) {
public GeoJsonMultiPoint convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonMultiPoint.TYPE), "does not contain a type 'MultiPoint'");
@@ -279,7 +279,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJsonLineString convert(Map<String, Object> source) {
public GeoJsonLineString convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonLineString.TYPE), "does not contain a type 'LineString'");
@@ -311,7 +311,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJsonMultiLineString convert(Map<String, Object> source) {
public GeoJsonMultiLineString convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonMultiLineString.TYPE), "does not contain a type 'MultiLineString'");
@@ -339,7 +339,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJsonPolygon convert(Map<String, Object> source) {
public GeoJsonPolygon convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonPolygon.TYPE), "does not contain a type 'Polygon'");
@@ -368,7 +368,6 @@ public class GeoConverters {
List<Object> coordinates = source.getCoordinates().stream() //
.map(GeoJsonPolygonToMapConverter.INSTANCE::convert) //
.filter(Objects::nonNull) //
.map(it -> it.get("coordinates")) //
.collect(Collectors.toList()); //
map.put("coordinates", coordinates);
@@ -383,7 +382,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJsonMultiPolygon convert(Map<String, Object> source) {
public GeoJsonMultiPolygon convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonMultiPolygon.TYPE), "does not contain a type 'MultiPolygon'");
@@ -430,7 +429,7 @@ public class GeoConverters {
INSTANCE;
@Override
public GeoJsonGeometryCollection convert(Map<String, Object> source) {
public GeoJsonGeometryCollection convert(Map<String, @Nullable Object> source) {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonGeometryCollection.TYPE),
@@ -448,7 +447,7 @@ public class GeoConverters {
// endregion
// region helper functions
private static String getGeoJsonType(Map<String, Object> source) {
private static String getGeoJsonType(Map<String, @Nullable Object> source) {
Object type = source.get("type");
Assert.notNull(type, "Document to convert does not contain a type");
@@ -485,7 +484,7 @@ public class GeoConverters {
return map;
}
private static List<GeoJsonLineString> geoJsonLineStringsFromMap(Map<String, Object> source) {
private static List<GeoJsonLineString> geoJsonLineStringsFromMap(Map<String, @Nullable Object> source) {
Object coordinates = source.get("coordinates");
Assert.notNull(coordinates, "Document to convert does not contain coordinates");
Assert.isTrue(coordinates instanceof List, "coordinates must be a List");
@@ -24,7 +24,6 @@ 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.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
@@ -63,16 +62,7 @@ import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.CachingValueExpressionEvaluatorFactory;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.EntityInstantiator;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider;
import org.springframework.data.mapping.model.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.format.datetime.DateFormatterRegistrar;
import org.springframework.util.Assert;
@@ -114,9 +104,8 @@ public class MappingElasticsearchConverter
protected @Nullable Environment environment;
private final SpELContext spELContext = new SpELContext(new MapAccessor());
private final SpelExpressionParser expressionParser = new SpelExpressionParser();
private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory(
expressionParser, this, spELContext);
new SpelExpressionParser(), this, spELContext);
private final EntityInstantiators instantiators = new EntityInstantiators();
private final ElasticsearchTypeMapper typeMapper;
@@ -267,7 +256,7 @@ public class MappingElasticsearchConverter
@Nullable
@SuppressWarnings("unchecked")
private <R> R read(TypeInformation<R> typeInformation, Map<String, Object> source) {
private <R> R read(TypeInformation<R> typeInformation, Map<String, @Nullable Object> source) {
Assert.notNull(source, "Source must not be null!");
@@ -301,7 +290,7 @@ public class MappingElasticsearchConverter
}
@SuppressWarnings("unchecked")
private <R> R readMap(TypeInformation<?> type, Map<String, Object> source) {
private <R> R readMap(TypeInformation<?> type, Map<String, @Nullable Object> source) {
Assert.notNull(source, "Document must not be null!");
@@ -313,9 +302,10 @@ public class MappingElasticsearchConverter
Class<?> rawKeyType = keyType != null ? keyType.getType() : null;
Class<?> rawValueType = valueType != null ? valueType.getType() : null;
Map<Object, Object> map = CollectionFactory.createMap(mapType, rawKeyType, source.keySet().size());
Map<Object, @Nullable Object> map = (Map<Object, @Nullable Object>) CollectionFactory.createMap(mapType,
rawKeyType, source.keySet().size());
for (Entry<String, Object> entry : source.entrySet()) {
for (Entry<String, @Nullable Object> entry : source.entrySet()) {
if (typeMapper.isTypeKey(entry.getKey())) {
continue;
@@ -325,6 +315,7 @@ public class MappingElasticsearchConverter
if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) {
key = conversionService.convert(key, rawKeyType);
Assert.notNull(key, "converted key must not be null");
}
Object value = entry.getValue();
@@ -343,7 +334,7 @@ public class MappingElasticsearchConverter
return (R) map;
}
private <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) {
private <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, @Nullable Object> source) {
ElasticsearchPersistentEntity<?> targetEntity = computeClosestEntity(entity, source);
ValueExpressionEvaluator evaluator = expressionEvaluatorFactory.create(source);
@@ -582,7 +573,7 @@ public class MappingElasticsearchConverter
: TypeInformation.OBJECT;
Class<?> rawComponentType = componentType.getType();
Collection<Object> items = targetType.getType().isArray() //
Collection<@Nullable Object> items = targetType.getType().isArray() //
? new ArrayList<>(source.size()) //
: CollectionFactory.createCollection(collectionType, rawComponentType, source.size());
@@ -672,7 +663,7 @@ public class MappingElasticsearchConverter
*/
private <T> void populateScriptedFields(ElasticsearchPersistentEntity<?> entity, T result,
SearchDocument searchDocument) {
Map<String, List<Object>> fields = searchDocument.getFields();
Map<String, List<@Nullable Object>> fields = searchDocument.getFields();
entity.doWithProperties((SimplePropertyHandler) property -> {
if (property.isAnnotationPresent(ScriptedField.class)) {
ScriptedField scriptedField = property.findAnnotation(ScriptedField.class);
@@ -695,7 +686,7 @@ public class MappingElasticsearchConverter
* Compute the type to use by checking the given entity against the store type;
*/
private ElasticsearchPersistentEntity<?> computeClosestEntity(ElasticsearchPersistentEntity<?> entity,
Map<String, Object> source) {
Map<String, @Nullable Object> source) {
TypeInformation<?> typeToUse = typeMapper.readType(source);
@@ -770,7 +761,7 @@ public class MappingElasticsearchConverter
INSTANCE;
@Override
public <T> T getParameterValue(Parameter<T, ElasticsearchPersistentProperty> parameter) {
public <T> @Nullable T getParameterValue(Parameter<T, ElasticsearchPersistentProperty> parameter) {
return null;
}
}
@@ -822,7 +813,7 @@ public class MappingElasticsearchConverter
* @param typeInformation type information for the source
*/
@SuppressWarnings("unchecked")
private void writeInternal(@Nullable Object source, Map<String, Object> sink,
private void writeInternal(@Nullable Object source, Map<String, @Nullable Object> sink,
@Nullable TypeInformation<?> typeInformation) {
if (null == source) {
@@ -833,7 +824,7 @@ public class MappingElasticsearchConverter
Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(entityType, Map.class);
if (customTarget.isPresent()) {
Map<String, Object> result = conversionService.convert(source, Map.class);
Map<String, @Nullable Object> result = conversionService.convert(source, Map.class);
if (result != null) {
sink.putAll(result);
@@ -863,7 +854,7 @@ public class MappingElasticsearchConverter
* @param sink the destination
* @param entity entity for the source
*/
private void writeInternal(@Nullable Object source, Map<String, Object> sink,
private void writeInternal(@Nullable Object source, Map<String, @Nullable Object> sink,
@Nullable ElasticsearchPersistentEntity<?> entity) {
if (source == null) {
@@ -905,7 +896,7 @@ public class MappingElasticsearchConverter
* @param sink must not be {@literal null}.
* @param propertyType must not be {@literal null}.
*/
private Map<String, Object> writeMapInternal(Map<?, ?> source, Map<String, Object> sink,
private Map<String, @Nullable Object> writeMapInternal(Map<?, ?> source, Map<String, @Nullable Object> sink,
TypeInformation<?> propertyType) {
for (Map.Entry<?, ?> entry : source.entrySet()) {
@@ -922,7 +913,7 @@ public class MappingElasticsearchConverter
sink.put(simpleKey,
writeCollectionInternal(asCollection(value), propertyType.getMapValueType(), new ArrayList<>()));
} else {
Map<String, Object> document = Document.create();
Map<String, @Nullable Object> document = Document.create();
TypeInformation<?> valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType()
: TypeInformation.OBJECT;
writeInternal(value, document, valueTypeInfo);
@@ -966,7 +957,7 @@ public class MappingElasticsearchConverter
} else if (element instanceof Collection || elementType.isArray()) {
collection.add(writeCollectionInternal(asCollection(element), componentType, new ArrayList<>()));
} else {
Map<String, Object> document = Document.create();
Map<String, @Nullable Object> document = Document.create();
writeInternal(element, document, componentType);
collection.add(document);
}
@@ -1080,7 +1071,7 @@ public class MappingElasticsearchConverter
: mappingContext.getRequiredPersistentEntity(type);
Object existingValue = sink.get(property);
Map<String, Object> document = existingValue instanceof Map ? (Map<String, Object>) existingValue
Map<String, @Nullable Object> document = existingValue instanceof Map ? (Map<String, Object>) existingValue
: Document.create();
addCustomTypeKeyIfNecessary(value, document, TypeInformation.of(property.getRawType()));
@@ -1097,7 +1088,7 @@ public class MappingElasticsearchConverter
* @param sink must not be {@literal null}.
* @param type type to compare to
*/
private void addCustomTypeKeyIfNecessary(Object source, Map<String, Object> sink,
private void addCustomTypeKeyIfNecessary(Object source, Map<String, @Nullable Object> sink,
@Nullable TypeInformation<?> type) {
if (!writeTypeHints) {
@@ -1218,7 +1209,7 @@ public class MappingElasticsearchConverter
Assert.notNull(map, "Given map must not be null!");
Assert.notNull(property, "PersistentProperty must not be null!");
return writeMapInternal(map, new LinkedHashMap<>(map.size()), property.getTypeInformation());
return writeMapInternal(map, new LinkedHashMap(map.size()), property.getTypeInformation());
}
/**
@@ -1492,9 +1483,9 @@ public class MappingElasticsearchConverter
@SuppressWarnings("ClassCanBeRecord")
static class MapValueAccessor {
final Map<String, Object> target;
final Map<String, @Nullable Object> target;
MapValueAccessor(Map<String, Object> target) {
MapValueAccessor(Map<String, @Nullable Object> target) {
this.target = target;
}
@@ -1528,7 +1519,7 @@ public class MappingElasticsearchConverter
}
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<String, Object> source = target;
Map<String, @Nullable Object> source = target;
Object result = null;
while (parts.hasNext()) {
@@ -1559,11 +1550,11 @@ public class MappingElasticsearchConverter
target.put(property.getFieldName(), value);
}
private Map<String, Object> getAsMap(Object result) {
private Map<String, @Nullable Object> getAsMap(Object result) {
if (result instanceof Map) {
// noinspection unchecked
return (Map<String, Object>) result;
return (Map<String, @Nullable Object>) result;
}
throw new IllegalArgumentException(String.format("%s is not a Map.", result));
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.core.convert;
@@ -62,7 +62,7 @@ public class Explanation {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -81,7 +81,7 @@ public class Explanation {
@Override
public int hashCode() {
int result = (match ? 1 : 0);
int result = (Boolean.TRUE.equals(match) ? 1 : 0);
result = 31 * result + value.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + details.hashCode();
@@ -215,7 +215,7 @@ class MapDocument implements Document {
* @see java.util.Map#containsKey(java.lang.Object)
*/
@Override
public boolean containsKey(Object key) {
public boolean containsKey(@Nullable Object key) {
return documentAsMap.containsKey(key);
}
@@ -224,7 +224,7 @@ class MapDocument implements Document {
* @see java.util.Map#containsValue(java.lang.Object)
*/
@Override
public boolean containsValue(Object value) {
public boolean containsValue(@Nullable Object value) {
return documentAsMap.containsValue(value);
}
@@ -233,7 +233,7 @@ class MapDocument implements Document {
* @see java.util.Map#get(java.lang.Object)
*/
@Override
public Object get(Object key) {
public Object get(@Nullable Object key) {
return documentAsMap.get(key);
}
@@ -242,7 +242,7 @@ class MapDocument implements Document {
* @see java.lang.Object#getOrDefault(java.lang.Object, java.lang.Object)
*/
@Override
public Object getOrDefault(Object key, Object defaultValue) {
public Object getOrDefault(@Nullable Object key, @Nullable Object defaultValue) {
return documentAsMap.getOrDefault(key, defaultValue);
}
@@ -251,7 +251,7 @@ class MapDocument implements Document {
* @see java.util.Map#put(java.lang.Object, java.lang.Object)
*/
@Override
public Object put(String key, Object value) {
public Object put(String key, @Nullable Object value) {
return documentAsMap.put(key, value);
}
@@ -260,7 +260,7 @@ class MapDocument implements Document {
* @see java.util.Map#remove(java.lang.Object)
*/
@Override
public Object remove(Object key) {
public Object remove(@Nullable Object key) {
return documentAsMap.remove(key);
}
@@ -314,7 +314,7 @@ class MapDocument implements Document {
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
return documentAsMap.equals(o);
}
@@ -332,7 +332,8 @@ class MapDocument implements Document {
* @see java.util.Map#forEach(java.util.function.BiConsumer)
*/
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
public void forEach(
@SuppressWarnings("NullableProblems") BiConsumer<? super String, ? super @Nullable Object> action) {
documentAsMap.forEach(action);
}
@@ -41,7 +41,7 @@ public interface SearchDocument extends Document {
/**
* @return the fields for the search result, not {@literal null}
*/
Map<String, List<Object>> getFields();
Map<String, List<@Nullable Object>> getFields();
/**
* The first value of the given field.
@@ -50,7 +50,7 @@ public interface SearchDocument extends Document {
*/
@Nullable
default <V> V getFieldValue(final String name) {
List<Object> values = getFields().get(name);
List<@Nullable Object> values = getFields().get(name);
if (values == null || values.isEmpty()) {
return null;
}
@@ -64,7 +64,7 @@ public interface SearchDocument extends Document {
*/
@Nullable
default <V> List<V> getFieldValues(final String name) {
List<Object> values = getFields().get(name);
List<@Nullable Object> values = getFields().get(name);
if (values == null) {
return null;
}
@@ -74,8 +74,7 @@ public interface SearchDocument extends Document {
/**
* @return the sort values for the search hit
*/
@Nullable
default Object[] getSortValues() {
default Object @Nullable [] getSortValues() {
return null;
}
@@ -35,7 +35,7 @@ public class SearchDocumentAdapter implements SearchDocument {
private final float score;
private final Object[] sortValues;
private final Map<String, List<Object>> fields = new HashMap<>();
private final Map<String, List<@Nullable Object>> fields = new HashMap<>();
private final Document delegate;
private final Map<String, List<String>> highlightFields = new HashMap<>();
private final Map<String, SearchDocumentResponse> innerHits = new HashMap<>();
@@ -44,9 +44,11 @@ public class SearchDocumentAdapter implements SearchDocument {
@Nullable private final Map<String, Double> matchedQueries;
@Nullable private final String routing;
public SearchDocumentAdapter(Document delegate, float score, Object[] sortValues, Map<String, List<Object>> fields,
public SearchDocumentAdapter(Document delegate, float score, Object[] sortValues,
Map<String, List<@Nullable Object>> fields,
Map<String, List<String>> highlightFields, Map<String, SearchDocumentResponse> innerHits,
@Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation, @Nullable Map<String, Double> matchedQueries,
@Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation,
@Nullable Map<String, Double> matchedQueries,
@Nullable String routing) {
this.delegate = delegate;
@@ -74,7 +76,7 @@ public class SearchDocumentAdapter implements SearchDocument {
}
@Override
public Map<String, List<Object>> getFields() {
public Map<String, List<@Nullable Object>> getFields() {
return fields;
}
@@ -186,17 +188,17 @@ public class SearchDocumentAdapter implements SearchDocument {
}
@Override
public boolean containsKey(Object key) {
public boolean containsKey(@Nullable Object key) {
return delegate.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
public boolean containsValue(@Nullable Object value) {
return delegate.containsValue(value);
}
@Override
public Object get(Object key) {
public Object get(@Nullable Object key) {
if (delegate.containsKey(key)) {
return delegate.get(key);
@@ -207,12 +209,12 @@ public class SearchDocumentAdapter implements SearchDocument {
}
@Override
public Object put(String key, Object value) {
public @Nullable Object put(String key, @Nullable Object value) {
return delegate.put(key, value);
}
@Override
public Object remove(Object key) {
public Object remove(@Nullable Object key) {
return delegate.remove(key);
}
@@ -232,12 +234,12 @@ public class SearchDocumentAdapter implements SearchDocument {
}
@Override
public Collection<Object> values() {
public Collection<@Nullable Object> values() {
return delegate.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
public Set<Entry<String, @Nullable Object>> entrySet() {
return delegate.entrySet();
}
@@ -254,7 +256,7 @@ public class SearchDocumentAdapter implements SearchDocument {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
@@ -270,17 +272,17 @@ public class SearchDocumentAdapter implements SearchDocument {
}
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
public void forEach(@Nullable BiConsumer<? super String, ? super @Nullable Object> action) {
delegate.forEach(action);
}
@Override
public boolean remove(Object key, Object value) {
public boolean remove(@Nullable Object key, @Nullable Object value) {
return delegate.remove(key, value);
}
@Override
public String getRouting() {
public @Nullable String getRouting() {
return routing;
}
@@ -1,5 +1,18 @@
/**
* Classes related to the Document structure of Elasticsearch documents and search responses.
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.core.document;
@@ -31,10 +31,11 @@ import org.springframework.data.mapping.callback.EntityCallback;
public interface AfterLoadCallback<T> extends EntityCallback<Document> {
/**
* Entity callback method invoked after a domain object is materialized from a {@link Document}. Can return either the
* same or a modified instance of the {@link Document} object.
* Entity callback method invoked after a {@link Document} is read from Elasticsearch. Can return either the same or a
* modified instance of the {@link Document} object.
*
* @param document the document.
* @param type the type into which the document will be converted
* @param indexCoordinates of the index the document was read from.
* @return a possible modified or new {@link Document}.
*/
@@ -1,5 +1,18 @@
/**
* classes and interfaces related to Spring Data Elasticsearch events and callbacks.
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.core.event;
@@ -8,18 +8,27 @@ import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.ValueSerializer;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;
class PointSerializer extends ValueSerializer<Point> {
@Override
public void serialize(Point value, JsonGenerator gen, SerializationContext serializers) throws JacksonException {
public void serialize(Point value, @Nullable JsonGenerator gen, @Nullable SerializationContext serializers)
throws JacksonException {
Assert.notNull(gen, "gen must not be null");
gen.writePOJO(GeoPoint.fromPoint(value));
}
}
class PointDeserializer extends ValueDeserializer<Point> {
@Override
public Point deserialize(JsonParser p, DeserializationContext context) throws JacksonException {
public Point deserialize(@Nullable JsonParser p, @Nullable DeserializationContext context) throws JacksonException {
Assert.notNull(p, "p must not be null");
return GeoPoint.toPoint(p.readValueAs(GeoPoint.class));
}
}
@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@@ -68,7 +69,7 @@ public class GeoJsonGeometryCollection implements GeoJson<Iterable<GeoJson<?>>>
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;
@@ -122,7 +123,7 @@ public class GeoJsonLineString implements GeoJson<Iterable<Point>> {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;
@@ -81,7 +82,7 @@ public class GeoJsonMultiLineString implements GeoJson<Iterable<GeoJsonLineStrin
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;
@@ -122,7 +123,7 @@ public class GeoJsonMultiPoint implements GeoJson<Iterable<Point>> {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@@ -62,7 +63,7 @@ public class GeoJsonMultiPolygon implements GeoJson<Iterable<GeoJsonPolygon>> {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core.geo;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
/**
@@ -89,7 +90,7 @@ public class GeoJsonPoint implements GeoJson<List<Double>> {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;
@@ -207,7 +208,7 @@ public class GeoJsonPolygon implements GeoJson<Iterable<GeoJsonLineString>> {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core.geo;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
/**
@@ -63,7 +64,7 @@ public class GeoPoint {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
@@ -1,2 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.core.geo;
@@ -27,7 +27,7 @@ import org.springframework.util.Assert;
*/
public class AliasActionParameters {
private final String[] indices;
@Nullable private final String[] aliases;
private final String @Nullable [] aliases;
@Nullable private final Query filterQuery;
@Nullable private final Class<?> filterQueryClass;
@Nullable private final Boolean isHidden;
@@ -36,7 +36,7 @@ public class AliasActionParameters {
@Nullable private final String indexRouting;
@Nullable private final String searchRouting;
private AliasActionParameters(String[] indices, @Nullable String[] aliases, @Nullable Boolean isHidden,
private AliasActionParameters(String[] indices, String @Nullable [] aliases, @Nullable Boolean isHidden,
@Nullable Boolean isWriteIndex, @Nullable String routing, @Nullable String indexRouting,
@Nullable String searchRouting, @Nullable Query filterQuery, @Nullable Class<?> filterQueryClass) {
this.indices = indices;
@@ -66,7 +66,7 @@ public class AliasActionParameters {
return indices;
}
public String@Nullable[] getAliases() {
public String @Nullable [] getAliases() {
return aliases;
}
@@ -38,7 +38,7 @@ public class AliasActions {
*
* @param actions {@link AliasAction} elements
*/
public AliasActions(@Nullable AliasAction... actions) {
public AliasActions(AliasAction @Nullable... actions) {
add(actions);
}
@@ -52,10 +52,9 @@ public class AliasActions {
* @param actions elements to add
* @return this object
*/
public AliasActions add(@Nullable AliasAction... actions) {
public AliasActions add(AliasAction @Nullable... actions) {
if (actions != null) {
// noinspection NullableProblems
this.actions.addAll(Arrays.asList(actions));
}
@@ -40,7 +40,7 @@ public record ComponentTemplateRequestData(@Nullable Settings settings, @Nullabl
@Nullable private AliasActions aliasActions;
@Nullable private Boolean allowAutoCreate;
public Builder withSettings(Map<String, Object> settings) {
public Builder withSettings(Map<String, @Nullable Object> settings) {
this.settings = new Settings(settings);
return this;
}
@@ -0,0 +1,31 @@
/*
* 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 tools.jackson.databind.node.ObjectNode;
import org.springframework.data.elasticsearch.annotations.CustomIndexOption;
/**
* Writes the particular instance of the {@code CustomIndexOption} into the mapping, represented by objectNode.
*
* @author Andriy Redko
* @since 6.2
*/
public interface IndexOptionMapper {
void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode);
}
@@ -0,0 +1,301 @@
/*
* 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 tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.BigIntegerNode;
import tools.jackson.databind.node.BooleanNode;
import tools.jackson.databind.node.DecimalNode;
import tools.jackson.databind.node.NullNode;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.StringNode;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.annotations.CustomIndexOption;
/**
* The collection of predefined {@link IndexOptionMapper}s
*
* @author Andriy Redko
* @since 6.2
*/
public final class IndexOptionMappers {
private IndexOptionMappers() {}
/**
* Writes the {@link CustomIndexOption} instance into mapping as JSON string (or array of strings) property
*/
public static final class StringMapper implements IndexOptionMapper {
@Override
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
final String[] values = Objects.requireNonNull(indexOption.values(), "Values are required");
if (values.length == 1) {
writePropertyAsString(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
} else if (values.length > 1) {
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
} else {
removeProperty(indexOption.name(), objectNode);
}
}
}
/**
* Writes the {@link CustomIndexOption} instance into mapping as JSON number (or array of numbers) property
*/
public static final class NumberMapper implements IndexOptionMapper {
@Override
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
final @Nullable Number[] values = toNumbers(Objects.requireNonNull(indexOption.values(), "Values are required"));
if (values.length == 1) {
writePropertyAsNumber(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
} else if (values.length > 1) {
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
} else {
removeProperty(indexOption.name(), objectNode);
}
}
}
/**
* Writes the {@link CustomIndexOption} instance into mapping as JSON boolean (or array of booleans) property
*/
public static final class BooleanMapper implements IndexOptionMapper {
@Override
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
final @Nullable Boolean[] values = toBoolean(Objects.requireNonNull(indexOption.values(), "Values are required"));
if (values.length == 1) {
writePropertyAsBoolean(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
} else if (values.length > 1) {
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
} else {
removeProperty(indexOption.name(), objectNode);
}
}
}
/**
* Convert the array of strings to array of booleans.
*
* @param values array of strings
* @return array of booleans
*/
private static @Nullable Boolean[] toBoolean(@Nullable String[] values) {
return Arrays.stream(values).map(Boolean::valueOf).toArray(Boolean[]::new);
}
/**
* Convert the array of strings to array of numbers, throwing {@link NumberFormatException} if the conversion is not
* possible.
*
* @param values array of strings
* @return array of numbers
* @throws NumberFormatException
*/
private static @Nullable Number[] toNumbers(@Nullable String[] values) {
final @Nullable Number[] numbers = new Number[values.length];
for (int j = 0; j < values.length; ++j) {
Number number = null;
var value = values[j];
if (value != null) {
try {
number = new BigInteger(value);
} catch (NumberFormatException e) {
number = new BigDecimal(value);
}
}
numbers[j] = number;
}
return numbers;
}
/**
* Writes a property as a JSON boolean value.
*
* @param name property name
* @param override override if present
* @param value property value
* @param objectNode JSON object node
*/
private static void writePropertyAsBoolean(String name, boolean override, @Nullable Boolean value,
ObjectNode objectNode) {
final JsonNode node = BooleanNode.valueOf(value);
if (objectNode.has(name) && override) {
objectNode.replace(name, node);
} else {
objectNode.putIfAbsent(name, node);
}
}
/**
* Writes a property as a JSON number value.
*
* @param name property name
* @param override override if present
* @param value property value
* @param objectNode JSON object node
*/
private static void writePropertyAsNumber(String name, boolean override, @Nullable Number value,
ObjectNode objectNode) {
final JsonNode node = toJsonNode(value);
if (objectNode.has(name) && override) {
objectNode.replace(name, node);
} else {
objectNode.putIfAbsent(name, node);
}
}
/**
* Writes a property as a JSON string value.
*
* @param name property name
* @param override override if present
* @param value property value
* @param objectNode JSON object node
*/
private static void writePropertyAsString(String name, boolean override, String value, ObjectNode objectNode) {
final StringNode node = StringNode.valueOf(value);
if (objectNode.has(name) && override) {
objectNode.replace(name, node);
} else {
objectNode.putIfAbsent(name, node);
}
}
/**
* Removes the property if present.
*
* @param name property name
* @param objectNode JSON object node
*/
private static void removeProperty(String name, ObjectNode objectNode) {
if (objectNode.has(name)) {
objectNode.remove(name);
}
}
/**
* Writes a property as a JSON array of boolean values.
*
* @param name property name
* @param override override if present
* @param values property values
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, @Nullable Boolean[] values,
ObjectNode objectNode) {
writePropertyAsArray(
name,
override,
objectNode
.arrayNode()
.addAll(
Arrays.stream(values).map(b -> b != null ? BooleanNode.valueOf(b) : NullNode.getInstance()).toList()),
objectNode);
}
/**
* Writes a property as a JSON array of number values.
*
* @param name property name
* @param override override if present
* @param values property values
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, @Nullable Number[] values,
ObjectNode objectNode) {
writePropertyAsArray(
name,
override,
objectNode
.arrayNode()
.addAll(Arrays.stream(values).map(IndexOptionMappers::toJsonNode).toList()),
objectNode);
}
/**
* Writes a property as a JSON array of string values.
*
* @param name property name
* @param override override if present
* @param values property values
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, String[] values, ObjectNode objectNode) {
writePropertyAsArray(
name,
override,
objectNode
.arrayNode()
.addAll(Arrays.stream(values).map(StringNode::valueOf).toList()),
objectNode);
}
/**
* Writes a property as a JSON array.
*
* @param name property name
* @param override override if present
* @param arrayNode JSON array
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, ArrayNode arrayNode, ObjectNode objectNode) {
if (objectNode.has(name) && override) {
objectNode.replace(name, arrayNode);
} else {
objectNode.putIfAbsent(name, arrayNode);
}
}
/**
* Converts a number into appropriate JSON node. Only {@link DecimalNode} and {@link BigInteger} types are supported.
*
* @param value {@link Number} instance to convert
* @throws IllegalArgumentException
*/
private static JsonNode toJsonNode(@Nullable Number value) {
if (value == null) {
return NullNode.getInstance();
}
if (value instanceof BigDecimal d) {
return DecimalNode.valueOf(d);
} else if (value instanceof BigInteger i) {
return BigIntegerNode.valueOf(i);
} else {
// We only support BigDecimal or BigInteger numeric values, should never happen since the
// conversion is constrained within this class only.
throw new IllegalArgumentException("Only BigDecimal or BigInteger numbers are supported");
}
}
}
@@ -36,9 +36,7 @@ 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.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Transient;
import org.springframework.data.core.TypeInformation;
@@ -112,9 +110,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;
}
/**
@@ -275,7 +279,7 @@ public class MappingBuilder {
writeTypeHintMapping(propertiesNode);
if (entity != null) {
entity.doWithProperties((PropertyHandler<@NonNull ElasticsearchPersistentProperty>) property -> {
entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
try {
if (property.isAnnotationPresent(Transient.class) || isInIgnoreFields(property, parentFieldAnnotation)) {
return;
@@ -589,7 +593,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);
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -34,7 +35,9 @@ import org.springframework.util.StringUtils;
/**
* 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 +45,10 @@ import org.springframework.util.StringUtils;
* @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";
@@ -86,7 +90,7 @@ public final class MappingParameters {
private final String analyzer;
private final boolean coerce;
@Nullable private final String[] copyTo;
private final String @Nullable [] copyTo;
private final DateFormat[] dateFormats;
private final String[] dateFormatPatterns;
private final boolean docValues;
@@ -117,6 +121,7 @@ public final class MappingParameters {
private final TermVector termVector;
private final FieldType type;
private final String mappedTypeName;
private final CustomIndexOption[] customIndexOptions;
/**
* extracts the mapping parameters from the relevant annotations.
@@ -137,7 +142,7 @@ public final class MappingParameters {
}
}
private MappingParameters(Field field) {
protected MappingParameters(Field field) {
index = field.index();
store = field.store();
fielddata = field.fielddata();
@@ -182,9 +187,10 @@ public final class MappingParameters {
Assert.isTrue(field.enabled() || type == FieldType.Object, "enabled false is only allowed for field type object");
enabled = field.enabled();
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
customIndexOptions = field.customIndexOptions();
}
private MappingParameters(InnerField field) {
protected MappingParameters(InnerField field) {
index = field.index();
store = field.store();
fielddata = field.fielddata();
@@ -228,6 +234,7 @@ public final class MappingParameters {
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
enabled = true;
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
customIndexOptions = field.customIndexOptions();
}
public boolean isStore() {
@@ -416,5 +423,143 @@ public final class MappingParameters {
if (eagerGlobalOrdinals) {
objectNode.put(FIELD_PARAM_EAGER_GLOBAL_ORDINALS, eagerGlobalOrdinals);
}
// At last, check the custom index options
for (CustomIndexOption customIndexOption: customIndexOptions) {
final IndexOptionMapper mapper = BeanUtils.instantiateClass(customIndexOption.mapper());
mapper.writeIndexOptionTo(customIndexOption, objectNode);
}
}
protected String analyzer() {
return analyzer;
}
protected boolean coerce() {
return coerce;
}
protected String @Nullable [] 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;
}
protected CustomIndexOption[] customIndexOptions() {
return customIndexOptions;
}
}
@@ -0,0 +1,36 @@
/*
* 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;
/**
* 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}
*/
MappingParameters from(Annotation annotation);
}
@@ -102,7 +102,7 @@ public class PutTemplateRequest {
this.indexPatterns = indexPatterns;
}
public TemplateRequestBuilder withSettings(Map<String, Object> settings) {
public TemplateRequestBuilder withSettings(Map<String, @Nullable Object> settings) {
this.settings = new Settings(settings);
return this;
}
@@ -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(
@@ -21,6 +21,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.util.Assert;
@@ -34,7 +35,7 @@ public class Settings extends DefaultStringObjectMap<Settings> {
public Settings() {}
public Settings(Map<String, Object> map) {
public Settings(Map<String, @Nullable Object> map) {
super(map);
}
@@ -54,7 +55,7 @@ public class Settings extends DefaultStringObjectMap<Settings> {
}
@Override
public Object get(Object key) {
public Object get(@Nullable Object key) {
return containsKey(key) ? super.get(key) : path(key.toString());
}
@@ -75,11 +76,11 @@ public class Settings extends DefaultStringObjectMap<Settings> {
* taken from https://stackoverflow.com/a/29698326/4393565
*/
@SuppressWarnings("unchecked")
private static Map<?, ?> deepMerge(Map<String, Object> original, Map<String, Object> newMap) {
private static Map<?, ?> deepMerge(Map<String, @Nullable Object> original, Map<String, @Nullable Object> newMap) {
for (Object key : newMap.keySet()) {
if (newMap.get(key) instanceof Map && original.get(key) instanceof Map) {
Map<String, Object> originalChild = (Map<String, Object>) original.get(key);
Map<String, Object> newChild = (Map<String, Object>) newMap.get(key);
Map<String, @Nullable Object> originalChild = (Map<String, Object>) original.get(key);
Map<String, @Nullable Object> newChild = (Map<String, Object>) newMap.get(key);
original.put(key.toString(), deepMerge(originalChild, newChild));
} else if (newMap.get(key) instanceof List && original.get(key) instanceof List) {
List<Object> originalChild = (List<Object>) original.get(key);
@@ -112,9 +113,9 @@ public class Settings extends DefaultStringObjectMap<Settings> {
* flattens a Map<String, Object> to a stream of Map.Entry objects where the keys are the dot separated concatenated
* keys of sub map entries
*/
static private Stream<Map.Entry<String, Object>> doFlatten(Map.Entry<String, Object> entry) {
static private Stream<Map.Entry<String, @Nullable Object>> doFlatten(Map.Entry<String, @Nullable Object> entry) {
if (entry.getValue()instanceof Map<?, ?> nested) {
if (entry.getValue() instanceof Map<?, ?> nested) {
// noinspection unchecked
return nested.entrySet().stream() //
@@ -91,7 +91,7 @@ public class TemplateData {
return this;
}
public TemplateDataBuilder withSettings(Map<String, Object> settings) {
public TemplateDataBuilder withSettings(Map<String, @Nullable Object> settings) {
this.settings = new Settings(settings);
return this;
}
@@ -1,5 +1,18 @@
/**
* Classes related to Elasticsearch index management.
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.core.index;
@@ -64,7 +64,7 @@ public class JoinField<ID> {
}
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
@@ -0,0 +1,18 @@
/*
* Copyright 2022-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.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.core.join;
@@ -115,7 +115,7 @@ public class Alias {
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (!(o instanceof Alias that))
@@ -33,7 +33,7 @@ public class CreateIndexSettings {
private final IndexCoordinates indexCoordinates;
private final Set<Alias> aliases;
@Nullable private final Map<String, Object> settings;
@Nullable private final Map<String, @Nullable Object> settings;
@Nullable private final Document mapping;
@@ -58,7 +58,7 @@ public class CreateIndexSettings {
}
@Nullable
public Map<String, Object> getSettings() {
public Map<String, @Nullable Object> getSettings() {
return settings;
}
@@ -71,7 +71,7 @@ public class CreateIndexSettings {
private final IndexCoordinates indexCoordinates;
private final Set<Alias> aliases = new HashSet<>();
@Nullable private Map<String, Object> settings;
@Nullable private Map<String, @Nullable Object> settings;
@Nullable private Document mapping;
@@ -94,7 +94,7 @@ public class CreateIndexSettings {
return this;
}
public Builder withSettings(Map<String, Object> settings) {
public Builder withSettings(Map<String, @Nullable Object> settings) {
Assert.notNull(settings, "settings must not be null");
this.settings = settings;
@@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Dynamic;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
@@ -39,6 +40,7 @@ import org.springframework.data.mapping.model.FieldNamingStrategy;
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
* @author Subhobrata Dey
* @author Steven Pearce
*/
public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, ElasticsearchPersistentProperty> {
@@ -179,6 +181,12 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
*/
boolean writeTypeHints();
/**
* @return the {@link ElasticsearchServerType} for the server that stores this entity
* @since 6.2
*/
ElasticsearchServerType getServerType();
/**
* @return the {@code dynamic} mapping parameter value.
* @since 4.3
@@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core.mapping;
import java.util.Arrays;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@@ -55,7 +56,7 @@ public class IndexCoordinates {
* @since 4.2
*/
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())

Some files were not shown because too many files have changed in this diff Show More