1
0
mirror of synced 2026-08-31 21:24:28 +00:00

Compare commits

...

29 Commits

Author SHA1 Message Date
Peter-Josef Meisch cd1bd52466 Fix formatting of DateFormat.strict_date_optional_time_nanos.
Original Pull Request: #3337
Closes: #3334

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-08-30 18:14:45 +02:00
Peter-Josef Meisch cc7bd2b729 Upgrade to Elasticsearch 9.5.2.
Original Pull Request: #3336
Closes: #3333

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-08-29 13:45:39 +02:00
Peter-Josef Meisch 4d0ec05411 Fix script handling when building an UpdateQuery.
Original Pull Request #3331
Closes #3324

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-08-22 17:09:28 +02:00
Mark Paluch 2b402c7424 Prepare next development iteration.
See #3296
2026-08-21 11:01:30 +02:00
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
54 changed files with 1812 additions and 182 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
+5 -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,6 +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
+6 -7
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,12 +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 }}'
context-url: '${{ vars.ARTIFACTORY_CONTEXT_URL }}'
repository: '${{ vars.ARTIFACTORY_REPOSITORY }}'
project: '${{ vars.ARTIFACTORY_PROJECT }}'
settings-xml: '${{ vars.SETTINGS_XML }}'
-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.
+29 -7
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>6.1.0</version>
<version>6.2.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>4.1.0</version>
<version>4.2.0-SNAPSHOT</version>
</parent>
<name>Spring Data Elasticsearch</name>
@@ -18,11 +18,11 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>4.1.0</springdata.commons>
<springdata.commons>4.2.0-SNAPSHOT</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>9.4.2</elasticsearch-java>
<elasticsearch-rest-client>9.4.2</elasticsearch-rest-client>
<elasticsearch-java>9.5.2</elasticsearch-java>
<elasticsearch-rest-client>9.5.2</elasticsearch-rest-client>
<hoverfly>0.20.2</hoverfly>
<log4j>2.25.4</log4j>
@@ -376,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>
@@ -498,8 +508,20 @@
</profiles>
<repositories>
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
@@ -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,6 +1,16 @@
[[new-features]]
= What's new
[[new-features.6-2-0]]
== New in Spring Data Elasticsearch 6.2
* Upgrade to Elasticsearch 9.5.2
* 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])
* Fix script handling when building an UpdateQuery (https://github.com/spring-projects/spring-data-elasticsearch/issues/3324[GH #3324])
* Fix formatting of DateFormat.strict_date_optional_time_nanos (https://github.com/spring-projects/spring-data-elasticsearch/issues/3334[GH #3334])
[[new-features.6-1-0]]
== New in Spring Data Elasticsearch 6.1
@@ -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,6 +6,7 @@ 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.1 | 6.2.x | 9.5.2 | 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.xfootnote:oom[Out of maintenance] | 8.18.1 | 6.2.x
@@ -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}'
@@ -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();
}
@@ -88,7 +88,7 @@ public enum DateFormat {
* @since 5.3
*/
strict_date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), //
strict_date_optional_time_nanos("uuuu-MM-dd['T'HH:mm:ss.SSSSSSXXX]"), //
strict_date_optional_time_nanos("uuuu-MM-dd['T'HH:mm:ss.SSSSSSSSSXXX]"), //
date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), //
/**
* @since 5.3
@@ -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.
@@ -24,12 +24,18 @@ 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.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
@@ -85,22 +91,8 @@ final class DocumentAdapters {
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 = fromFields.apply(hit.fields());
EntityAsMap hitFieldsAsMap = new EntityAsMap();
hit.fields().forEach((key, jsonData) -> hitFieldsAsMap.put(key, toJavaObject(jsonData, jsonpMapper)));
Map<String, List<@Nullable Object>> documentFields = new LinkedHashMap<>();
hitFieldsAsMap.forEach((key, value) -> {
@@ -144,6 +136,61 @@ final class DocumentAdapters {
documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
}
@Nullable
private static Object toJavaObject(JsonData jsonData, JsonpMapper jsonpMapper) {
return toJavaObject(jsonData.toJson(jsonpMapper));
}
@Nullable
private static Object toJavaObject(JsonValue jsonValue) {
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;
};
}
private static Map<String, @Nullable Object> toMap(JsonObject jsonObject) {
Map<String, @Nullable Object> result = new LinkedHashMap<>();
jsonObject.forEach((key, value) -> result.put(key, toJavaObject(value)));
return result;
}
private static List<@Nullable Object> toList(JsonArray jsonArray) {
List<@Nullable Object> result = new ArrayList<>(jsonArray.size());
jsonArray.forEach(value -> result.add(toJavaObject(value)));
return result;
}
private static Number toNumber(JsonNumber jsonNumber) {
if (!jsonNumber.isIntegral()) {
return jsonNumber.doubleValue();
}
try {
return jsonNumber.intValueExact();
} catch (ArithmeticException ignored) {
// continue with a wider numeric type
}
try {
return jsonNumber.longValueExact();
} catch (ArithmeticException ignored) {
// continue with a wider numeric type
}
BigInteger value = jsonNumber.bigIntegerValue();
return value;
}
public static SearchDocument from(CompletionSuggestOption<EntityAsMap> completionSuggestOption) {
Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source())
@@ -70,11 +70,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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;
@@ -234,7 +234,8 @@ class RequestConverter extends AbstractQueryProcessor {
for (Alias alias : indexSettings.getAliases()) {
co.elastic.clients.elasticsearch.indices.Alias esAlias = co.elastic.clients.elasticsearch.indices.Alias
.of(ab -> {
co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder = ab.filter(getQuery(alias.getFilter(), null))
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);
@@ -807,7 +808,7 @@ class RequestConverter extends AbstractQueryProcessor {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
return GetRequest.of(grb -> {
GetRequest.Builder builder = grb //
GetRequest.Builder builder = grb //
.index(indexCoordinates.getIndexName()) //
.id(id); //
getRouting(routing).ifPresent(builder::routing);
@@ -846,7 +847,7 @@ class RequestConverter extends AbstractQueryProcessor {
List<MultiGetOperation> multiGetOperations = query.getIdsWithRouting().stream()
.map(idWithRouting -> MultiGetOperation.of(mgo -> {
MultiGetOperation.Builder builder = mgo //
MultiGetOperation.Builder builder = mgo //
.index(index.getIndexName()) //
.id(idWithRouting.id()) //
.source(sourceConfig);
@@ -1071,29 +1072,7 @@ class RequestConverter extends AbstractQueryProcessor {
return UpdateRequest.of(uqb -> {
uqb.index(indexName).id(query.getId());
var scriptData = query.getScriptData();
var script = scriptData != null ? scriptData.script() : null;
if (script != null) {
Map<String, JsonData> params = new HashMap<>();
if (query.getParams() != null) {
query.getParams().forEach((key, value) -> params.put(key, JsonData.of(value, jsonpMapper)));
}
uqb.script(sb -> {
sb
.lang(scriptData.language())
.params(params);
if (script != null) {
sb.source(s -> s.scriptString(script));
}
sb.id(scriptData.scriptName());
return sb;
});
}
uqb.script(getScript(query.getScriptData()));
uqb
.doc(query.getDocument())
@@ -1980,6 +1959,7 @@ class RequestConverter extends AbstractQueryProcessor {
}
return Optional.empty();
}
Optional<String> getRouting(@Nullable String routing1, @Nullable String routing2) {
return getRouting(routing1).or(() -> getRouting(routing2));
}
@@ -93,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
@@ -42,6 +42,7 @@ import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Steven Pearce
* @since 3.2
*/
@Configuration(proxyBeanMethods = false)
@@ -73,6 +74,7 @@ public class ElasticsearchConfigurationSupport {
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
mappingContext.setWriteTypeHints(writeTypeHints());
mappingContext.setServerType(serverType());
return mappingContext;
}
@@ -174,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.
@@ -185,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,
}
@@ -104,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;
@@ -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");
}
}
}
@@ -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;
@@ -120,6 +121,7 @@ public 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.
@@ -185,6 +187,7 @@ public 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();
}
protected MappingParameters(InnerField field) {
@@ -231,6 +234,7 @@ public class MappingParameters {
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
enabled = true;
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
customIndexOptions = field.customIndexOptions();
}
public boolean isStore() {
@@ -419,6 +423,12 @@ public 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() {
@@ -548,4 +558,8 @@ public class MappingParameters {
protected String mappedTypeName() {
return mappedTypeName;
}
protected CustomIndexOption[] customIndexOptions() {
return customIndexOptions;
}
}
@@ -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
@@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core.mapping;
import org.jspecify.annotations.Nullable;
import org.springframework.data.core.TypeInformation;
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.Property;
@@ -31,6 +32,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
* @author Mohsin Husen
* @author Mark Paluch
* @author Peter-Josef Meisch
* @author Steven Pearce
*/
public class SimpleElasticsearchMappingContext
extends AbstractMappingContext<SimpleElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> {
@@ -39,6 +41,7 @@ public class SimpleElasticsearchMappingContext
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
private boolean writeTypeHints = true;
private ElasticsearchServerType serverType = ElasticsearchServerType.DEFAULT;
/**
* Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied.
@@ -61,6 +64,18 @@ public class SimpleElasticsearchMappingContext
this.writeTypeHints = writeTypeHints;
}
/**
* Sets the ElasticSearch Server Type
*
* @param serverType the {@link ElasticsearchServerType} to be used to set the server Type. The Default value will
* support the standard configurations, and SERVERLESS will support ElasticSearch Serverless
*
* @since 6.2
*/
public void setServerType(@Nullable ElasticsearchServerType serverType) {
this.serverType = serverType == null ? ElasticsearchServerType.DEFAULT : serverType;
}
@Override
protected boolean shouldCreatePersistentEntityFor(TypeInformation<?> type) {
return !ElasticsearchSimpleTypes.HOLDER.isSimpleType(type.getType());
@@ -69,7 +84,7 @@ public class SimpleElasticsearchMappingContext
@Override
protected <T> SimpleElasticsearchPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
return new SimpleElasticsearchPersistentEntity<>(typeInformation,
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints));
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints, serverType));
}
@Override
@@ -33,24 +33,26 @@ import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.annotations.Setting;
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.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.utils.spel.ExpressionUtils;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Elasticsearch specific {@link org.springframework.data.mapping.PersistentEntity} implementation holding
@@ -64,15 +66,15 @@ import org.springframework.util.Assert;
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
* @author Subhobrata Dey
* @author Steven Pearce
*/
public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntity<T, ElasticsearchPersistentProperty>
implements ElasticsearchPersistentEntity<T> {
private static final Log LOGGER = LogFactory.getLog(SimpleElasticsearchPersistentEntity.class);
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private @Nullable final Document document;
private @Nullable String indexName;
private final String unresolvedIndexName;
private final Lazy<SettingsParameter> settingsParameter;
private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty;
private @Nullable ElasticsearchPersistentProperty joinFieldProperty;
@@ -82,14 +84,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private final boolean alwaysWriteMapping;
private final Dynamic dynamic;
private final Map<String, ElasticsearchPersistentProperty> fieldNamePropertyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Expression> routingExpressions = new ConcurrentHashMap<>();
private @Nullable String routing;
private final ContextConfiguration contextConfiguration;
private final Set<Alias> aliases = new HashSet<>();
private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
private final boolean storeIdInSource;
private final boolean storeVersionInSource;
@@ -108,10 +105,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
this.settingsParameter = Lazy.of(() -> buildSettingsParameter(clazz));
if (document != null) {
Assert.hasText(document.indexName(),
" Unknown indexName. Make sure the indexName is defined. e.g @Document(indexName=\"foo\")");
this.indexName = document.indexName();
this.unresolvedIndexName = document.indexName();
this.versionType = document.versionType();
this.createIndexAndMapping = document.createIndex();
this.alwaysWriteMapping = document.alwaysWriteMapping();
@@ -120,6 +117,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
this.storeVersionInSource = document.storeVersionInSource();
buildAliases();
} else {
this.unresolvedIndexName = getTypeInformation().getType().getSimpleName();
this.dynamic = Dynamic.INHERIT;
this.storeIdInSource = true;
this.storeVersionInSource = true;
@@ -136,13 +134,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
}
}
private String getIndexName() {
return indexName != null ? indexName : getTypeInformation().getType().getSimpleName();
}
@Override
public IndexCoordinates getIndexCoordinates() {
return resolve(IndexCoordinates.of(getIndexName()));
return resolve(IndexCoordinates.of(unresolvedIndexName));
}
@Override
@@ -174,7 +168,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
@Nullable
@Override
public String getRefreshInterval() {
return settingsParameter.get().refreshIntervall;
return settingsParameter.get().refreshInterval;
}
@Override
@@ -196,6 +190,11 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return contextConfiguration.getFieldNamingStrategy();
}
@Override
public ElasticsearchServerType getServerType() {
return contextConfiguration.getServerType();
}
@Override
public boolean writeTypeHints() {
@@ -368,52 +367,55 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
Assert.notNull(name, "name must not be null");
Expression expression = getExpressionForIndexName(name);
ValueExpression expression = ExpressionUtils.detectExpression(name);
String resolvedName = expression != null ? expression.getValue(indexNameEvaluationContext.get(), String.class)
: null;
return resolvedName != null ? resolvedName : name;
Object resolvedName;
if (expression != null && !expression.isLiteral()) {
var valueEvaluationContext = getValueEvaluationContext(name);
resolvedName = expression.evaluate(valueEvaluationContext);
} else {
resolvedName = null;
}
return resolvedName != null ? ObjectUtils.nullSafeToString(resolvedName) : name;
}
/**
* returns an {@link Expression} for #name if name contains a {@link ParserContext#TEMPLATE_EXPRESSION} otherwise
* returns {@literal null}.
* build the {@link ValueEvaluationContext} from the expression.
*
* @param name the name to get the expression for
* @return Expression may be null
* @return ValueEvaluationContext
* @param expression
*/
@Nullable
private Expression getExpressionForIndexName(String name) {
return indexNameExpressions.computeIfAbsent(name, s -> {
Expression expr = PARSER.parseExpression(s, ParserContext.TEMPLATE_EXPRESSION);
return expr instanceof LiteralExpression ? null : expr;
});
private ValueEvaluationContext getValueEvaluationContext(@Nullable String expression) {
ValueExpression valueExpression = ExpressionUtils.detectExpression(expression);
return getValueEvaluationContext(valueExpression);
}
/**
* build the {@link EvaluationContext} considering {@link ExpressionDependencies} from the name returned by
* {@link #getIndexName()}.
* build the {@link ValueEvaluationContext} considering {@link ExpressionDependencies} from the expression.
*
* @return EvaluationContext
* @return ValueEvaluationContext
* @param expression
*/
private EvaluationContext getIndexNameEvaluationContext() {
private ValueEvaluationContext getValueEvaluationContext(@Nullable ValueExpression expression) {
Expression expression = getExpressionForIndexName(getIndexName());
ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression)
: ExpressionDependencies.none();
var expressionDependencies = expression != null ? expression.getExpressionDependencies() : null;
// noinspection ConstantConditions
return getEvaluationContext(null, expressionDependencies);
return expressionDependencies != null ? getValueEvaluationContext(null, expressionDependencies)
: getValueEvaluationContext((Object) null);
}
@Override
@Nullable
public String resolveRouting(T bean) {
// we need a @Routing annotation on the entity
if (routing == null) {
return null;
}
// if there is a property with name of of the @Routing annotation value, return the property's value
ElasticsearchPersistentProperty persistentProperty = getPersistentProperty(routing);
if (persistentProperty != null) {
@@ -422,14 +424,29 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return propertyValue != null ? propertyValue.toString() : null;
}
// try to resolve the annotations value
try {
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression);
ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression);
var routingValue = routing;
ValueExpression expression = ExpressionUtils.detectExpression(routingValue);
EvaluationContext context = getEvaluationContext(null, expressionDependencies);
context.setVariable("entity", bean);
if (expression == null) {
return null;
}
return expression.getValue(context, String.class);
if (expression.isLiteral()) {
// before using ValueExpressions, we had SpEL expressions without the "#{...},
// so try if we get a valid expression after wrapping that
routingValue = "#{" + routing + "}";
expression = ExpressionUtils.detectExpression(routingValue);
if (expression == null) {
return null;
}
}
ValueEvaluationContext context = getValueEvaluationContext(routingValue);
context.getEvaluationContext().setVariable("entity", bean);
return ObjectUtils.nullSafeToString(expression.evaluate(context));
} catch (EvaluationException e) {
throw new InvalidDataAccessApiUsageException(
"Could not resolve expression: " + routing + " for object of class " + bean.getClass().getCanonicalName(), e);
@@ -447,9 +464,13 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
}
try {
Expression expression = PARSER.parseExpression(settingPathFromParameter, ParserContext.TEMPLATE_EXPRESSION);
return (expression instanceof LiteralExpression) ? settingPathFromParameter
: expression.getValue(getEvaluationContext(null, ExpressionDependencies.discover(expression)), String.class);
ValueExpression expression = ExpressionUtils.detectExpression(settingPathFromParameter);
if (expression == null || expression instanceof LiteralExpression) {
return settingPathFromParameter;
}
return ObjectUtils.nullSafeToString(expression.evaluate(getValueEvaluationContext(settingPathFromParameter)));
} catch (ExpressionException e) {
throw new InvalidDataAccessApiUsageException(
"Could not resolve expression: " + settingPathFromParameter + " for @Setting.settingPath ", e);
@@ -468,9 +489,13 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
// default values
settingsParameter.useServerConfiguration = false;
settingsParameter.serverType = contextConfiguration.serverType;
settingsParameter.shards = 1;
settingsParameter.replicas = 1;
settingsParameter.refreshIntervall = "1s";
settingsParameter.refreshInterval = switch (contextConfiguration.serverType) {
case DEFAULT -> "1s";
case SERVERLESS -> "5s";
};
if (settingAnnotation != null) {
processSettingAnnotation(settingAnnotation, settingsParameter);
@@ -484,7 +509,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
settingsParameter.settingPath = settingAnnotation.settingPath();
settingsParameter.shards = settingAnnotation.shards();
settingsParameter.replicas = settingAnnotation.replicas();
settingsParameter.refreshIntervall = settingAnnotation.refreshInterval();
if (StringUtils.hasText(settingAnnotation.refreshInterval())) {
settingsParameter.refreshInterval = settingAnnotation.refreshInterval();
}
settingsParameter.indexStoreType = settingAnnotation.indexStoreType();
String[] sortFields = settingAnnotation.sortFields();
@@ -560,10 +587,11 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
*/
private static class SettingsParameter {
boolean useServerConfiguration = false;
ElasticsearchServerType serverType = ElasticsearchServerType.DEFAULT;
@Nullable String settingPath;
short shards;
short replicas;
@Nullable String refreshIntervall;
@Nullable String refreshInterval;
@Nullable String indexStoreType;
private String @Nullable [] sortFields;
private Setting.@Nullable SortOrder @Nullable [] sortOrders;
@@ -576,12 +604,16 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return new Settings();
}
var index = new Settings() //
.append("number_of_shards", String.valueOf(shards)) //
.append("number_of_replicas", String.valueOf(replicas));
var index = new Settings();
if (refreshIntervall != null) {
index.append("refresh_interval", refreshIntervall);
if (ElasticsearchServerType.DEFAULT.equals(serverType)) {
index
.append("number_of_shards", String.valueOf(shards))
.append("number_of_replicas", String.valueOf(replicas));
}
if (refreshInterval != null) {
index.append("refresh_interval", refreshInterval);
}
if (indexStoreType != null && !"fs".equals(indexStoreType)) {
@@ -619,10 +651,17 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private final FieldNamingStrategy fieldNamingStrategy;
private final boolean writeTypeHints;
private final ElasticsearchServerType serverType;
ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints) {
this(fieldNamingStrategy, writeTypeHints, ElasticsearchServerType.DEFAULT);
}
ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints,
ElasticsearchServerType serverType) {
this.fieldNamingStrategy = fieldNamingStrategy;
this.writeTypeHints = writeTypeHints;
this.serverType = serverType;
}
public FieldNamingStrategy getFieldNamingStrategy() {
@@ -632,6 +671,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
public boolean getWriteTypeHints() {
return writeTypeHints;
}
public ElasticsearchServerType getServerType() {
return serverType;
}
}
@Override
@@ -24,7 +24,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.PartTree;
/**
* A repository query that is built from the the method name in the repository definition. Was originally named
* A repository query that is built from the method name in the repository definition. Was originally named
* ElasticsearchPartQuery.
*
* @author Rizwan Idrees
@@ -0,0 +1,83 @@
/*
* Copyright 2021-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.utils.spel;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Contract;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.StringUtils;
/**
* Internal utility class for dealing with {@link ValueExpression} and potential ones. Shamelessly copied from Spring
* Data MongoDB. Adapted afterwards to our needs.
*
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @since 6.2
*/
public final class ExpressionUtils {
private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
public static final int CAPACITY = 256;
private static final ConcurrentLruCache<String, ValueExpression> expressionCache = new ConcurrentLruCache<>(
CAPACITY, PARSER::parse);
/**
* Returns a SpEL {@link ValueExpression} if the given {@link String} is not empty. ValueExpressions are stored in a
* cash.
*
* @param potentialExpression can be {@literal null}
* @return {@link ValueExpression} or null when input is empty
*/
@Contract("null -> null")
public static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
return expressionCache.get(potentialExpression);
}
/**
* evbaluates value against the context provided by valueEvaluationContextSupplier. If value cannot be transformed
* into a {@link ValueExpression}, the defaultValue is returned.
*
* @param value the value to evaluate
* @param valueEvaluationContextSupplier supplies the context to use for evaluation
* @param defaultValue
* @return
*/
public static @Nullable Object evaluate(String value, Supplier<ValueEvaluationContext> valueEvaluationContextSupplier,
@Nullable Object defaultValue) {
ValueExpression expression = detectExpression(value);
if (expression == null) {
return defaultValue;
}
var evaluated = expression.evaluate(valueEvaluationContextSupplier.get());
return evaluated == null ? defaultValue : evaluated;
}
}
+3 -17
View File
@@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
@@ -178,7 +179,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@@ -186,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -199,18 +200,3 @@
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.
=======================================================================
To the extent any open source subcomponents are licensed under the EPL and/or other
similar licenses that require the source code and/or modifications to
source code to be made available (as would be noted above), you may obtain a
copy of the source code corresponding to the binaries for such open source
components and modifications thereto, if any, (the "Source Files"), by
downloading the Source Files from https://www.springsource.org/download,
or by sending a request, with your name and address to: VMware, Inc., 3401 Hillview
Avenue, Palo Alto, CA 94304, United States of America or email info@vmware.com. All
such requests should clearly specify: OPEN SOURCE FILES REQUEST, Attention General
Counsel. VMware shall mail a copy of the Source Files to you on a CD or equivalent
physical medium. This offer to obtain a copy of the Source Files is valid for three
years from the date you acquired this Software product.
+2 -2
View File
@@ -1,5 +1,5 @@
Spring Data Elasticsearch 6.1 GA (2026.0.0)
Copyright (c) [2013-2022] Pivotal Software, Inc.
Spring Data Elasticsearch 6.2.0-M1 (2026.1.0)
Copyright (c) [2013-2026] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
You may not use this product except in compliance with the License.
@@ -16,7 +16,6 @@
package org.springframework.data.elasticsearch.client;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static io.specto.hoverfly.junit.dsl.HoverflyDsl.*;
import static io.specto.hoverfly.junit.verification.HoverflyVerifications.*;
@@ -32,6 +31,7 @@ import io.specto.hoverfly.junit5.api.HoverflyConfig;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -108,6 +108,8 @@ public class RestClientsTest {
AtomicInteger connectionConfigurerCount = new AtomicInteger(0);
AtomicInteger connectionManagerConfigurerCount = new AtomicInteger(0);
AtomicInteger requestConfigurerCount = new AtomicInteger(0);
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);
AtomicReference<String> supplierThreadName = new AtomicReference<>();
ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder();
configurationBuilder //
@@ -115,7 +117,9 @@ public class RestClientsTest {
.withBasicAuth("user", "password") //
.withDefaultHeaders(defaultHeaders) //
.withHeaders(() -> {
supplierThreadName.set(Thread.currentThread().getName());
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("thread", threadLocal.get());
httpHeaders.add("supplied", "val0");
httpHeaders.add("supplied", "val" + supplierCount.getAndIncrement());
return httpHeaders;
@@ -179,6 +183,7 @@ public class RestClientsTest {
// do several calls to check that the headerSupplier provided values are set
int startValue = clientUnderTest.usesInitialRequest() ? 2 : 1;
for (int i = startValue; i <= startValue + 2; i++) {
threadLocal.set("local");
clientUnderTest.ping();
verify(headRequestedFor(urlEqualTo("/")) //
@@ -189,10 +194,15 @@ public class RestClientsTest {
.withHeader("supplied", new EqualToPattern("val0")) //
// on the first call Elasticsearch does the version check and thus already increments the counter
.withHeader("supplied", new EqualToPattern("val" + i)) //
.withHeader("supplied", including("val0", "val" + i)));
.withHeader("supplied", including("val0", "val" + i)) //
.withHeader("thread", new EqualToPattern("local")));
;
}
// the headers supplier must run on the calling thread, so a ThreadLocal set above is visible (#3300).
// this guards against future regressions that move the supplier back onto the async I/O thread.
assertThat(supplierThreadName.get()).isEqualTo(Thread.currentThread().getName());
assertThat(restClientConfigurerCount).hasValue(clientUnderTestFactory.getExpectedRestClientConfigurerCalls());
assertThat(httpClientConfigurerCount).hasValue(1);
assertThat(connectionConfigurerCount).hasValue(clientUnderTestFactory.getExpectedConnectionConfigurerCalls());
@@ -19,6 +19,7 @@ import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import jakarta.json.JsonValue;
import java.util.Arrays;
import java.util.Collections;
@@ -77,6 +78,49 @@ class DocumentAdaptersUnitTests {
softly.assertAll();
}
@Test // #3178
@DisplayName("should adapt parser backed search Hit fields to Java types")
void shouldAdaptParserBackedSearchHitFieldsToJavaTypes() {
Hit<EntityAsMap> searchHit = new Hit.Builder<EntityAsMap>() //
.index("index") //
.id("my-id") //
.fields("objectField", JsonData.fromJson(
"{\"string\":\"value\",\"integer\":2,\"decimal\":1.5,\"nested\":{\"flag\":true}}")) //
.fields("listField", JsonData.fromJson("[\"listValue\",{\"nested\":3},true,null]")) //
.fields("stringField", JsonData.fromJson("\"stringValue\"")) //
.build(); //
SearchDocument document = DocumentAdapters.from(searchHit, jsonpMapper);
SoftAssertions softly = new SoftAssertions();
Object objectFieldValue = document.get("objectField");
Object listFieldValue = document.get("listField");
Object objectFieldFirstValue = document.getFieldValue("objectField");
Object listFieldFirstValue = document.getFieldValue("listField");
softly.assertThat(objectFieldValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
softly.assertThat(listFieldValue).isInstanceOf(List.class).isNotInstanceOf(JsonValue.class);
softly.assertThat(document.get("stringField")).isEqualTo("stringValue");
softly.assertThat(objectFieldFirstValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
softly.assertThat(listFieldFirstValue).isEqualTo("listValue");
// noinspection unchecked
Map<String, Object> objectField = (Map<String, Object>) objectFieldValue;
softly.assertThat(objectField.get("string")).isEqualTo("value");
softly.assertThat(objectField.get("integer")).isEqualTo(2);
softly.assertThat(objectField.get("decimal")).isEqualTo(1.5d);
softly.assertThat(objectField.get("nested")).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
// noinspection unchecked
List<Object> listField = (List<Object>) listFieldValue;
softly.assertThat(listField).containsExactly("listValue", Collections.singletonMap("nested", 3), true, null);
softly.assertThat(listField.get(1)).isNotInstanceOf(JsonValue.class);
softly.assertAll();
}
@Test // #1973
@DisplayName("should adapt search Hit from source")
void shouldAdaptSearchHitFromSource() {
@@ -16,6 +16,10 @@
package org.springframework.data.elasticsearch.client.elc;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -131,10 +135,28 @@ class RequestConverterTest {
.documentUpdateRequest(updateQuery, IndexCoordinates.of("foo"), null, null);
assertThat(updateRequest.id()).isEqualTo("queryId");
assertThat(updateRequest.script()).isNotNull();
assertThat(updateRequest.script().source().scriptString()).isEqualTo("script");
assertThat(updateRequest.script().id()).isEqualTo("scriptName");
}
@Test // #3324
@DisplayName("should allow update script with only an id and no script code")
void shouldAllowUpdateScriptWithOnlyAnIdAndNoScriptCode() {
var updateQuery = UpdateQuery
.builder("queryId")
.withScriptName("scriptName")
.build();
UpdateRequest<org.springframework.data.elasticsearch.core.document.Document, ?> updateRequest = requestConverter
.documentUpdateRequest(updateQuery, IndexCoordinates.of("foo"), null, null);
assertThat(updateRequest.id()).isEqualTo("queryId");
assertThat(updateRequest.script()).isNotNull();
assertThat(updateRequest.script().source()).isNull();
assertThat(updateRequest.script().id()).isEqualTo("scriptName");
}
@Test
void getRouting() {
@@ -148,5 +170,4 @@ class RequestConverterTest {
assertEquals("5", requestConverter.getRouting(null, "5").get());
}
}
@@ -27,7 +27,8 @@ import org.springframework.data.elasticsearch.annotations.DateFormat;
*/
class ElasticsearchDateConverterUnitTests {
private final ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin"));
public static final ZoneId zoneEuropeBerlin = ZoneId.of("Europe/Berlin");
private final ZonedDateTime zdt = ZonedDateTime.now(zoneEuropeBerlin);
@ParameterizedTest // DATAES-716
@EnumSource(DateFormat.class)
@@ -274,10 +275,14 @@ class ElasticsearchDateConverterUnitTests {
check(ElasticsearchDateConverter.of(DateFormat.date_optional_time), LocalDateTime.class);
}
@Test // #2676
@Test // #2676, #3334
@DisplayName("should convert strict_date_optional_time_nanos")
void shouldConvertStrictDateOptionalTime() {
check(ElasticsearchDateConverter.of(DateFormat.strict_date_optional_time_nanos), LocalDateTime.class);
LocalDateTime parsed = check(ElasticsearchDateConverter.of(DateFormat.strict_date_optional_time_nanos),
LocalDateTime.class);
// additional check that all nanoseconds are re-read
var zoned = ZonedDateTime.of(parsed, zoneEuropeBerlin);
assertThat(zoned).isEqualTo(zdt);
}
@Test // #1647
@@ -418,11 +423,12 @@ class ElasticsearchDateConverterUnitTests {
check(ElasticsearchDateConverter.of("basic_date_time ||invalid-pattern"), LocalDateTime.class);
}
private <T extends TemporalAccessor> void check(ElasticsearchDateConverter converter, Class<T> type) {
private <T extends TemporalAccessor> T check(ElasticsearchDateConverter converter, Class<T> type) {
String formatted = converter.format(zdt);
T parsed = converter.parse(formatted, type);
assertThat(parsed).isNotNull();
return parsed;
}
}
@@ -41,6 +41,9 @@ import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.BooleanMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.NumberMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.StringMapper;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@@ -273,6 +276,12 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
void shouldWriteCorrectMappingForDenseVectorProperty() {
operations.indexOps(SimilarityEntity.class).createWithMapping();
}
@Test // #3299
@DisplayName("should write correct mapping for dense vector property with custom index options")
void shouldWriteCorrectMappingForDenseVectorPropertyWithCustomIndexOptions() {
operations.indexOps(SimilarityCustomIndexOptionEntity.class).createWithMapping();
}
@Test // #2845
@DisplayName("should write mapping with field aliases")
@@ -280,6 +289,12 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
operations.indexOps(FieldAliasEntity.class).createWithMapping();
}
@Test // #3298
@DisplayName("should write index prefixes with default value")
void shouldWriteIndexPrefixesWithDefaultValue() {
operations.indexOps(TextIndexPrefixesEntity.class).createWithMapping();
}
// region Entities
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class Book {
@@ -908,6 +923,22 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
@Field(name = "dotted.field", type = Text) private String dottedField;
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class SimilarityCustomIndexOptionEntity {
@Nullable
@Id private String id;
@Field(type = FieldType.Dense_Vector, dims = 42,
knnSimilarity = KnnSimilarity.COSINE,
customIndexOptions = {
@CustomIndexOption(name = "similarity", values = "l2_norm", overrideIfPresent = true, mapper = StringMapper.class),
@CustomIndexOption(name = "element_type", values = "bit", overrideIfPresent = false, mapper = StringMapper.class),
@CustomIndexOption(name = "dims", values = "64", overrideIfPresent = true, mapper = NumberMapper.class),
@CustomIndexOption(name = "index", values = "true", overrideIfPresent = true, mapper = BooleanMapper.class)
}) private double @Nullable [] denseVector;
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class SimilarityEntity {
@Nullable
@@ -931,5 +962,14 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
@Field(type = Text) private String otherText;
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
private static class TextIndexPrefixesEntity {
@Id
@Nullable private String id;
@Nullable
// the min value is set to the default
@Field(type = Text, indexPrefixes = @IndexPrefixes(maxChars = 10)) private String someText;
}
// endregion
}
@@ -41,6 +41,9 @@ import org.springframework.data.domain.Range;
import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.BooleanMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.NumberMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.StringMapper;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.suggest.Completion;
@@ -1371,6 +1374,37 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
assertEquals(expected, mapping, false);
}
@Test // #3299
@DisplayName("should write dense_vector properties for knn search with custom index options")
void shouldWriteDenseVectorPropertiesWithKnnSearchCustomIndexOptions() throws JSONException {
String expected = """
{
"properties":{
"my_vector":{
"dims":32,
"element_type":"bit",
"similarity":"dot_product",
"index": true,
"floats": [0.5644, 0.4445, 0.95442],
"ints": [-1, 0, 1],
"strings": ["s1", "", "s2"],
"bools": [true, false],
"index_options":{
"type":"hnsw",
"m":16,
"ef_construction":100
}
}
}
}
""";
String mapping = getMappingBuilder().buildPropertyMapping(DenseVectorEntityWithKnnSearchCustomIndexOptions.class);
assertEquals(expected, mapping, false);
}
// region entities
@Document(indexName = "ignore-above-index")
@@ -2248,6 +2282,45 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
}
}
@SuppressWarnings("unused")
static class DenseVectorEntityWithKnnSearchCustomIndexOptions {
@Nullable
@Id private String id;
@Field(type = FieldType.Dense_Vector, dims = 16,
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100),
knnSimilarity = KnnSimilarity.DOT_PRODUCT,
customIndexOptions = {
@CustomIndexOption(name = "similarity", values = "l2_norm", mapper = StringMapper.class),
@CustomIndexOption(name = "element_type", values = "bit", mapper = StringMapper.class),
@CustomIndexOption(name = "dims", values = "32", overrideIfPresent = true, mapper = NumberMapper.class),
@CustomIndexOption(name = "index", values = "true", mapper = BooleanMapper.class),
@CustomIndexOption(name = "floats", values = { "0.5644", "0.4445", "0.95442" }, mapper = NumberMapper.class),
@CustomIndexOption(name = "ints", values = { "-1", "0", "1" }, mapper = NumberMapper.class),
@CustomIndexOption(name = "strings", values = { "s1", "", "s2" }, mapper = StringMapper.class),
@CustomIndexOption(name = "bools", values = { "false", "true" }, mapper = BooleanMapper.class),
@CustomIndexOption(name = "type", mapper = StringMapper.class)
}
) private float @Nullable [] my_vector;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
public float @Nullable [] getMy_vector() {
return my_vector;
}
public void setMy_vector(float @Nullable [] my_vector) {
this.my_vector = my_vector;
}
}
@Mapping(enabled = false)
static class DisabledMappingEntity {
@Nullable
@@ -0,0 +1,33 @@
/*
* Copyright 2024-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.mapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
public class SimpleElasticsearchPersistentEntityELCIntegrationTests extends SimpleElasticsearchPersistentEntityIntegrationTests {
@Configuration
static class Config extends ElasticsearchTemplateConfiguration {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("persistent-entity");
}
}
}
@@ -0,0 +1,100 @@
/*
* Copyright 2021-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.mapping;
/*
* Copyright 2024-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.
*/
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.TestPropertySource;
/**
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
@TestPropertySource(properties = { "entity.indexName = index-property" })
public abstract class SimpleElasticsearchPersistentEntityIntegrationTests {
@Autowired private ElasticsearchOperations operations;
@Autowired private IndexNameProvider indexNameProvider;
@BeforeEach
void setUp() {
indexNameProvider.increment();
}
@Test // #3310
@DisplayName("should evaluate static index name")
void shouldEvaluateStaticIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithStaticName.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("static-name");
}
@Test // #3310
@DisplayName("should evaluate SpEL index name")
void shouldEvaluateSpELIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithSpel.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo(indexNameProvider.indexName());
}
@Test // #3310
@DisplayName("should evaluate property index name")
void shouldEvaluatePropertyIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithProperty.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("index-property");
}
private ElasticsearchPersistentEntity<?> getRequiredPersistentEntity(Class<?> clazz) {
return operations.getElasticsearchConverter().getMappingContext().getRequiredPersistentEntity(clazz);
}
@Document(indexName = "static-name")
static class EntityWithStaticName {}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class EntityWithSpel {}
@Document(indexName = "${entity.indexName}")
static class EntityWithProperty {}
}
@@ -36,7 +36,12 @@ import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.annotations.WriteTypeHint;
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.model.FieldNamingStrategy;
@@ -53,6 +58,7 @@ import org.springframework.util.ReflectionUtils;
* @author Oliver Gierke
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
* @author Steven Pearce
*/
public class SimpleElasticsearchPersistentEntityTests extends MappingContextBaseTests {
@@ -229,6 +235,34 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
assertEquals(expected, json, false);
}
@Test
@DisplayName("should write Default parameters to Settings object")
void shouldWriteDefaultParametersToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.get().getMappingContext()
.getRequiredPersistentEntity(SettingDefaults.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).containsEntry("index.number_of_shards", "1");
assertThat(settings).containsEntry("index.number_of_replicas", "1");
assertThat(settings).containsEntry("index.refresh_interval", "1s");
}
@Test
@DisplayName("should write Shard and Replica parameters to Settings object")
void shouldWriteShardReplicaToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.get().getMappingContext()
.getRequiredPersistentEntity(SettingWithShardReplicas.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).containsEntry("index.number_of_shards", "4");
assertThat(settings).containsEntry("index.number_of_replicas", "5");
assertThat(settings).containsEntry("index.refresh_interval", "1s");
}
@Test // #3187
@DisplayName("should evaluate SpEL expression in settingPath")
void shouldEvaluateSpElExpressionInSettingPath() {
@@ -248,6 +282,61 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
}
}
@Nested
@DisplayName("serverless index settings")
@SpringJUnitConfig({ ServerlessSettingsTests.Config.class })
class ServerlessSettingsTests {
@Autowired private ElasticsearchConverter elasticsearchServerlessConverter;
@Configuration
static class Config {
@Bean
ElasticsearchConverter setupElasticsearchServerlessConverter() {
return new MappingElasticsearchConverter(setupMappingContext());
}
private SimpleElasticsearchMappingContext setupMappingContext() {
ElasticsearchConfigurationSupport configurationSupport = new ElasticsearchConfigurationSupport();
SimpleElasticsearchMappingContext mappingContext = configurationSupport
.elasticsearchMappingContext(configurationSupport.elasticsearchCustomConversions());
mappingContext.setServerType(ElasticsearchServerType.SERVERLESS);
mappingContext.initialize();
return mappingContext;
}
}
@Test
@DisplayName("should write Default parameters to Settings object")
void shouldWriteDefaultParametersToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchServerlessConverter.getMappingContext()
.getRequiredPersistentEntity(SettingDefaults.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).doesNotContainKey("index.number_of_shards");
assertThat(settings).doesNotContainKey("index.number_of_replicas");
assertThat(settings).containsEntry("index.refresh_interval", "5s");
}
@Test
@DisplayName("should not write Shard and Replica parameters to Settings object")
void shouldNotWriteShardReplicaToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchServerlessConverter.getMappingContext()
.getRequiredPersistentEntity(SettingWithShardReplicas.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).doesNotContainKey("index.number_of_shards");
assertThat(settings).doesNotContainKey("index.number_of_replicas");
assertThat(settings).containsEntry("index.refresh_interval", "5s");
}
}
@Nested
@DisplayName("configuration")
class ConfigurationTests {
@@ -310,6 +399,57 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
assertThat(entity.writeTypeHints()).isTrue();
}
@Test
@DisplayName("should return Default ElasticsearchServerType from context configuration")
void shouldReturnDefaultElasticsearchServerTypeFromContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(FieldNameEntity.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.DEFAULT);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("1s");
}
@Test
@DisplayName("should return ElasticsearchServerType from context configuration")
void shouldReturnElasticsearchServerTypeFromContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setServerType(ElasticsearchServerType.SERVERLESS);
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(FieldNameEntity.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("5s");
}
@Test
@DisplayName("should return OverriddenRefreshInterval from DEFAULT context configuration")
void shouldReturnOverriddenRefreshIntervalFromDEFAULTContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setServerType(ElasticsearchServerType.SERVERLESS);
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(SettingWithRefreshInterval.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("9s");
}
@Test
@DisplayName("should return OverriddenRefreshInterval from SERVERLESS context configuration")
void shouldReturnOverriddenRefreshIntervalFromSERVERLESSContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setServerType(ElasticsearchServerType.SERVERLESS);
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(SettingWithRefreshInterval.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("9s");
}
}
// region helper
@@ -446,5 +586,25 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Nullable
@Id String id;
}
@Document(indexName = "foo")
private static class SettingDefaults {
@Nullable
@Id String id;
}
@Document(indexName = "foo")
@Setting(refreshInterval = "9s")
private static class SettingWithRefreshInterval {
@Nullable
@Id String id;
}
@Document(indexName = "foo")
@Setting(shards = 4, replicas = 5)
private static class SettingWithShardReplicas {
@Nullable
@Id String id;
}
// endregion
}
@@ -89,6 +89,17 @@ class DefaultRoutingResolverUnitTest {
assertThat(routing).isEqualTo("route 42");
}
@Test // #3321
@DisplayName("should return routing from Value expression")
void shouldReturnRoutingFromValueExpression() {
ValidValueRoutingEntity entity = new ValidValueRoutingEntity("42", "route 42");
String routing = routingResolver.getRouting(entity);
assertThat(routing).isEqualTo("route 42");
}
@Document(indexName = "routing-resolver-test")
@Routing("theRouting")
static class ValidRoutingEntity {
@@ -151,6 +162,37 @@ class DefaultRoutingResolverUnitTest {
}
}
@Document(indexName = "routing-resolver-test")
@Routing(value = "#{@spelRouting.getRouting(#entity)}")
static class ValidValueRoutingEntity {
@Nullable
@Id private String id;
@Nullable private String theRouting;
public ValidValueRoutingEntity(@Nullable String id, @Nullable String theRouting) {
this.id = id;
this.theRouting = theRouting;
}
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getTheRouting() {
return theRouting;
}
public void setTheRouting(@Nullable String theRouting) {
this.theRouting = theRouting;
}
}
@Document(indexName = "routing-resolver-test")
@Routing("unknownProperty")
static class InvalidRoutingEntity {
@@ -187,8 +229,12 @@ class DefaultRoutingResolverUnitTest {
@Nullable
public String getRouting(Object o) {
if (o instanceof ValidSpelRoutingEntity) {
return ((ValidSpelRoutingEntity) o).getTheRouting();
if (o instanceof ValidSpelRoutingEntity e) {
return e.getTheRouting();
}
if (o instanceof ValidValueRoutingEntity e) {
return e.getTheRouting();
}
return null;
@@ -15,7 +15,7 @@
#
#
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
sde.testcontainers.image-version=9.4.2
sde.testcontainers.image-version=9.5.2
#
#
# needed as we do a DELETE /* at the end of the tests, will be required from 8.0 on, produces a warning since 7.13