Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62091be997 | |||
| fdc7893817 | |||
| 535d76faf0 | |||
| 26bd770b8c | |||
| aec03a3529 | |||
| e3b26b2268 | |||
| 6f3941b43b | |||
| bfd3c35d93 | |||
| f5b29cb524 | |||
| 7f5bfffc34 | |||
| 61176940cb | |||
| 24618ecfbe | |||
| 3e2c67a39f | |||
| d2ab03e6a4 | |||
| 172933af8e | |||
| 378dcabe19 | |||
| 893c9cbf92 | |||
| 3157c62198 | |||
| fe8c2b13b0 | |||
| 98716a871b | |||
| d55947b81e | |||
| 6cb5f92928 | |||
| b4ab1f28cd | |||
| aab66c9595 | |||
| d06c122fd5 | |||
| b1b232d354 | |||
| 555b570246 | |||
| 81eb167981 | |||
| 6ad98bf500 | |||
| 9149c1bc2e | |||
| d079a59cb4 | |||
| 7a7145e5b1 | |||
| dbf932cb20 | |||
| 738ee54a25 | |||
| 03992ba722 | |||
| 06de217ceb | |||
| eba8eec6c3 | |||
| 3fc19bbe8c | |||
| 8f8600727c | |||
| 95e028a1e9 | |||
| dd156b9e29 | |||
| 8d0ecf2aa3 | |||
| 4cc80abcd8 | |||
| eca6a7ec77 | |||
| d9d1b73dad | |||
| d101eebc6d | |||
| 4ef5af1f2d | |||
| fade919be6 | |||
| 687b014e70 | |||
| 9d139299b2 | |||
| 161439ae22 | |||
| fbe54e485b | |||
| 86e0e660be | |||
| 5ebe9f4492 | |||
| e997b39f68 | |||
| 41e32576e3 | |||
| 82c4ea1391 |
+1
-2
@@ -30,7 +30,6 @@ target
|
||||
build/
|
||||
node_modules
|
||||
node
|
||||
package.json
|
||||
package-lock.json
|
||||
|
||||
.mvn/.gradle-enterprise
|
||||
.mvn/.develocity
|
||||
|
||||
+3
-8
@@ -1,13 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>com.gradle</groupId>
|
||||
<artifactId>gradle-enterprise-maven-extension</artifactId>
|
||||
<version>1.19.2</version>
|
||||
</extension>
|
||||
<extension>
|
||||
<groupId>com.gradle</groupId>
|
||||
<artifactId>common-custom-user-data-maven-extension</artifactId>
|
||||
<version>1.12.4</version>
|
||||
<groupId>io.spring.develocity.conventions</groupId>
|
||||
<artifactId>develocity-conventions-maven-extension</artifactId>
|
||||
<version>0.0.19</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<gradleEnterprise
|
||||
xmlns="https://www.gradle.com/gradle-enterprise-maven" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://www.gradle.com/gradle-enterprise-maven https://www.gradle.com/schema/gradle-enterprise-maven.xsd">
|
||||
<server>
|
||||
<url>https://ge.spring.io</url>
|
||||
</server>
|
||||
<buildScan>
|
||||
<backgroundBuildScanUpload>false</backgroundBuildScanUpload>
|
||||
<captureGoalInputFiles>true</captureGoalInputFiles>
|
||||
<publishIfAuthenticated>true</publishIfAuthenticated>
|
||||
<obfuscation>
|
||||
<ipAddresses>#{{'0.0.0.0'}}</ipAddresses>
|
||||
</obfuscation>
|
||||
</buildScan>
|
||||
<buildCache>
|
||||
<local>
|
||||
<enabled>true</enabled>
|
||||
</local>
|
||||
<remote>
|
||||
<server>
|
||||
<credentials>
|
||||
<username>${env.GRADLE_ENTERPRISE_CACHE_USERNAME}</username>
|
||||
<password>${env.GRADLE_ENTERPRISE_CACHE_PASSWORD}</password>
|
||||
</credentials>
|
||||
</server>
|
||||
<enabled>true</enabled>
|
||||
<storeEnabled>#{env['GRADLE_ENTERPRISE_CACHE_USERNAME'] != null and env['GRADLE_ENTERPRISE_CACHE_PASSWORD'] != null}</storeEnabled>
|
||||
</remote>
|
||||
</buildCache>
|
||||
</gradleEnterprise>
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
#Thu Dec 14 08:40:44 CET 2023
|
||||
#Thu Nov 07 09:47:28 CET 2024
|
||||
wrapperUrl=https\://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
|
||||
Vendored
+25
-23
@@ -9,7 +9,7 @@ pipeline {
|
||||
|
||||
triggers {
|
||||
pollSCM 'H/10 * * * *'
|
||||
upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS)
|
||||
upstream(upstreamProjects: "spring-data-commons/3.4.x", threshold: hudson.model.Result.SUCCESS)
|
||||
}
|
||||
|
||||
options {
|
||||
@@ -33,15 +33,16 @@ pipeline {
|
||||
|
||||
environment {
|
||||
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
|
||||
DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}")
|
||||
DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) {
|
||||
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
|
||||
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
|
||||
docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
|
||||
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) {
|
||||
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
|
||||
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,14 +64,15 @@ pipeline {
|
||||
options { timeout(time: 30, unit: 'MINUTES') }
|
||||
environment {
|
||||
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
|
||||
DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}")
|
||||
DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) {
|
||||
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
|
||||
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
|
||||
docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
|
||||
docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) {
|
||||
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
|
||||
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,24 +94,24 @@ pipeline {
|
||||
options { timeout(time: 20, unit: 'MINUTES') }
|
||||
environment {
|
||||
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
|
||||
DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}")
|
||||
DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) {
|
||||
sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' +
|
||||
"DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " +
|
||||
"DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " +
|
||||
"GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " +
|
||||
"./mvnw -s settings.xml -Pci,artifactory -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch-non-root " +
|
||||
"-Dartifactory.server=${p['artifactory.url']} " +
|
||||
"-Dartifactory.username=${ARTIFACTORY_USR} " +
|
||||
"-Dartifactory.password=${ARTIFACTORY_PSW} " +
|
||||
"-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " +
|
||||
"-Dartifactory.build-name=spring-data-elasticsearch " +
|
||||
"-Dartifactory.build-number=spring-data-elasticsearch-${BRANCH_NAME}-build-${BUILD_NUMBER} " +
|
||||
"-Dmaven.test.skip=true clean deploy -U -B"
|
||||
docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
|
||||
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) {
|
||||
sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' +
|
||||
"./mvnw -s settings.xml -Pci,artifactory " +
|
||||
"-Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root " +
|
||||
"-Dartifactory.server=${p['artifactory.url']} " +
|
||||
"-Dartifactory.username=${ARTIFACTORY_USR} " +
|
||||
"-Dartifactory.password=${ARTIFACTORY_PSW} " +
|
||||
"-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " +
|
||||
"-Dartifactory.build-name=spring-data-elasticsearch " +
|
||||
"-Dartifactory.build-number=spring-data-elasticsearch-${BRANCH_NAME}-build-${BUILD_NUMBER} " +
|
||||
"-Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch " +
|
||||
"-Dmaven.test.skip=true clean deploy -U -B"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -2,12 +2,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR}
|
||||
export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW}
|
||||
export JENKINS_USER=${JENKINS_USER_NAME}
|
||||
|
||||
# The environment variable to configure access key is still GRADLE_ENTERPRISE_ACCESS_KEY
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY}
|
||||
|
||||
MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \
|
||||
./mvnw -s settings.xml clean -Dscan=false -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch
|
||||
./mvnw -s settings.xml clean -Dscan=false -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
# Java versions
|
||||
java.main.tag=17.0.9_9-jdk-focal
|
||||
java.next.tag=21.0.1_12-jdk-jammy
|
||||
java.main.tag=17.0.13_11-jdk-focal
|
||||
java.next.tag=23.0.1_11-jdk-noble
|
||||
|
||||
# Docker container images - standard
|
||||
docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.main.tag}
|
||||
docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag}
|
||||
docker.java.main.image=library/eclipse-temurin:${java.main.tag}
|
||||
docker.java.next.image=library/eclipse-temurin:${java.next.tag}
|
||||
|
||||
# Supported versions of MongoDB
|
||||
docker.mongodb.4.4.version=4.4.25
|
||||
docker.mongodb.5.0.version=5.0.21
|
||||
docker.mongodb.6.0.version=6.0.10
|
||||
docker.mongodb.7.0.version=7.0.2
|
||||
docker.mongodb.8.0.version=8.0.0
|
||||
|
||||
# Supported versions of Redis
|
||||
docker.redis.6.version=6.2.13
|
||||
docker.redis.7.version=7.2.4
|
||||
|
||||
# Supported versions of Cassandra
|
||||
docker.cassandra.3.version=3.11.16
|
||||
@@ -25,9 +27,10 @@ docker.java.inside.docker=-u root -v /var/run/docker.sock:/var/run/docker.sock -
|
||||
# Credentials
|
||||
docker.registry=
|
||||
docker.credentials=hub.docker.com-springbuildmaster
|
||||
docker.proxy.registry=https://docker-hub.usw1.packages.broadcom.com
|
||||
docker.proxy.credentials=usw1_packages_broadcom_com-jenkins-token
|
||||
artifactory.credentials=02bd1690-b54f-4c9f-819d-a77cb7a9822c
|
||||
artifactory.url=https://repo.spring.io
|
||||
artifactory.repository.snapshot=libs-snapshot-local
|
||||
develocity.cache.credentials=gradle_enterprise_cache_user
|
||||
develocity.access-key=gradle_enterprise_secret_access_key
|
||||
jenkins.user.name=spring-builds+jenkins
|
||||
|
||||
+1
-8
@@ -3,15 +3,8 @@
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p /tmp/jenkins-home/.m2/spring-data-elasticsearch
|
||||
chown -R 1001:1001 .
|
||||
|
||||
export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR}
|
||||
export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW}
|
||||
export JENKINS_USER=${JENKINS_USER_NAME}
|
||||
|
||||
# The environment variable to configure access key is still GRADLE_ENTERPRISE_ACCESS_KEY
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY}
|
||||
|
||||
MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \
|
||||
./mvnw -s settings.xml \
|
||||
-P${PROFILE} clean dependency:list verify -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch
|
||||
-P${PROFILE} clean dependency:list verify -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.6",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.7",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.13.0",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.11"
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-elasticsearch</artifactId>
|
||||
<version>5.3.0</version>
|
||||
<version>5.4.1</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<version>3.4.1</version>
|
||||
</parent>
|
||||
|
||||
<name>Spring Data Elasticsearch</name>
|
||||
@@ -18,17 +18,16 @@
|
||||
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
|
||||
|
||||
<properties>
|
||||
<springdata.commons>3.3.0</springdata.commons>
|
||||
<springdata.commons>3.4.1</springdata.commons>
|
||||
|
||||
<!-- version of the ElasticsearchClient -->
|
||||
<elasticsearch-java>8.13.2</elasticsearch-java>
|
||||
<elasticsearch-java>8.15.5</elasticsearch-java>
|
||||
|
||||
<blockhound-junit>1.0.8.RELEASE</blockhound-junit>
|
||||
<hoverfly>0.14.4</hoverfly>
|
||||
<log4j>2.18.0</log4j>
|
||||
<jsonassert>1.5.1</jsonassert>
|
||||
<testcontainers>1.18.0</testcontainers>
|
||||
<wiremock>2.35.1</wiremock>
|
||||
<hoverfly>0.19.0</hoverfly>
|
||||
<log4j>2.23.1</log4j>
|
||||
<jsonassert>1.5.3</jsonassert>
|
||||
<testcontainers>1.20.0</testcontainers>
|
||||
<wiremock>3.9.1</wiremock>
|
||||
|
||||
<java-module-name>spring.data.elasticsearch</java-module-name>
|
||||
|
||||
@@ -132,17 +131,6 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.elasticsearch.client</groupId>
|
||||
<artifactId>elasticsearch-rest-client</artifactId> <!-- is Apache 2-->
|
||||
<version>${elasticsearch-java}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson JSON Mapper -->
|
||||
<dependency>
|
||||
@@ -248,13 +236,6 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.tools</groupId>
|
||||
<artifactId>blockhound-junit-platform</artifactId>
|
||||
<version>${blockhound-junit}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.skyscreamer</groupId>
|
||||
<artifactId>jsonassert</artifactId>
|
||||
@@ -263,8 +244,8 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<groupId>org.wiremock</groupId>
|
||||
<artifactId>wiremock</artifactId>
|
||||
<version>${wiremock}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
@@ -443,25 +424,6 @@
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>jdk13+</id>
|
||||
<!-- on jDK13+, Blockhound needs this JVM flag set -->
|
||||
<activation>
|
||||
<jdk>[13,)</jdk>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-XX:+AllowRedefinitionToAddDeleteMethods</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>antora-process-resources</id>
|
||||
<build>
|
||||
@@ -479,7 +441,7 @@
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.spring.maven.antora</groupId>
|
||||
<groupId>org.antora</groupId>
|
||||
<artifactId>antora-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
# The purpose of this Antora playbook is to build the docs in the current branch.
|
||||
antora:
|
||||
extensions:
|
||||
- '@antora/collector-extension'
|
||||
- require: '@springio/antora-extensions/root-component-extension'
|
||||
- require: '@springio/antora-extensions'
|
||||
root_component_name: 'data-elasticsearch'
|
||||
site:
|
||||
title: Spring Data Elasticsearch
|
||||
@@ -22,13 +21,12 @@ content:
|
||||
start_path: src/main/antora
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-pagination: ''
|
||||
hide-uri-scheme: '@'
|
||||
tabs-sync-option: '@'
|
||||
chomp: 'all'
|
||||
extensions:
|
||||
- '@asciidoctor/tabs'
|
||||
- '@springio/asciidoctor-extensions'
|
||||
- '@springio/asciidoctor-extensions/javadoc-extension'
|
||||
sourcemap: true
|
||||
urls:
|
||||
latest_version_segment: ''
|
||||
@@ -38,5 +36,5 @@ runtime:
|
||||
format: pretty
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
|
||||
snapshot: true
|
||||
|
||||
@@ -10,3 +10,8 @@ ext:
|
||||
local: true
|
||||
scan:
|
||||
dir: target/classes/
|
||||
- run:
|
||||
command: ./mvnw package -Pdistribute
|
||||
local: true
|
||||
scan:
|
||||
dir: target/antora
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
*** xref:migration-guides/migration-guide-4.4-5.0.adoc[]
|
||||
*** xref:migration-guides/migration-guide-5.0-5.1.adoc[]
|
||||
*** xref:migration-guides/migration-guide-5.1-5.2.adoc[]
|
||||
*** xref:migration-guides/migration-guide-5.2-5.3.adoc[]
|
||||
*** xref:migration-guides/migration-guide-5.3-5.4.adoc[]
|
||||
|
||||
|
||||
* xref:elasticsearch.adoc[]
|
||||
@@ -39,4 +41,5 @@
|
||||
** xref:repositories/query-keywords-reference.adoc[]
|
||||
** xref:repositories/query-return-types-reference.adoc[]
|
||||
|
||||
* https://github.com/spring-projects/spring-data-commons/wiki[Wiki]
|
||||
* xref:attachment$api/java/index.html[Javadoc,role=link-external,window=_blank]
|
||||
* https://github.com/spring-projects/spring-data-commons/wiki[Wiki,role=link-external,window=_blank]
|
||||
|
||||
@@ -31,7 +31,7 @@ public class MyClientConfig extends ElasticsearchConfiguration {
|
||||
<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration]
|
||||
====
|
||||
|
||||
The `ElasticsearchConfiguration` class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
|
||||
The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
|
||||
|
||||
|
||||
The following beans can then be injected in other Spring components:
|
||||
@@ -52,13 +52,13 @@ RestClient restClient; <.>
|
||||
JsonpMapper jsonpMapper; <.>
|
||||
----
|
||||
|
||||
<.> an implementation of `ElasticsearchOperations`
|
||||
<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[]
|
||||
<.> the `co.elastic.clients.elasticsearch.ElasticsearchClient` that is used.
|
||||
<.> the low level `RestClient` from the Elasticsearch libraries
|
||||
<.> the `JsonpMapper` user by the Elasticsearch `Transport`
|
||||
====
|
||||
|
||||
Basically one should just use the `ElasticsearchOperations` to interact with the Elasticsearch cluster.
|
||||
Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] to interact with the Elasticsearch cluster.
|
||||
When using repositories, this instance is used under the hood as well.
|
||||
|
||||
[[elasticsearch.clients.reactiverestclient]]
|
||||
@@ -86,7 +86,7 @@ public class MyClientConfig extends ReactiveElasticsearchConfiguration {
|
||||
<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration]
|
||||
====
|
||||
|
||||
The `ReactiveElasticsearchConfiguration` class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
|
||||
The javadoc:org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
|
||||
|
||||
The following beans can then be injected in other Spring components:
|
||||
|
||||
@@ -108,20 +108,20 @@ JsonpMapper jsonpMapper; <.>
|
||||
|
||||
the following can be injected:
|
||||
|
||||
<.> an implementation of `ReactiveElasticsearchOperations`
|
||||
<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[]
|
||||
<.> the `org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient` that is used.
|
||||
This is a reactive implementation based on the Elasticsearch client implementation.
|
||||
<.> the low level `RestClient` from the Elasticsearch libraries
|
||||
<.> the `JsonpMapper` user by the Elasticsearch `Transport`
|
||||
====
|
||||
|
||||
Basically one should just use the `ReactiveElasticsearchOperations` to interact with the Elasticsearch cluster.
|
||||
Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[] to interact with the Elasticsearch cluster.
|
||||
When using repositories, this instance is used under the hood as well.
|
||||
|
||||
[[elasticsearch.clients.configuration]]
|
||||
== Client Configuration
|
||||
|
||||
Client behaviour can be changed via the `ClientConfiguration` that allows to set options for SSL, connect and socket timeouts, headers and other parameters.
|
||||
Client behaviour can be changed via the javadoc:org.springframework.data.elasticsearch.client.ClientConfiguration[] that allows to set options for SSL, connect and socket timeouts, headers and other parameters.
|
||||
|
||||
.Client Configuration
|
||||
====
|
||||
@@ -178,7 +178,7 @@ If this is used in the reactive setup, the supplier function *must not* block!
|
||||
[[elasticsearch.clients.configuration.callbacks]]
|
||||
=== Client configuration callbacks
|
||||
|
||||
The `ClientConfiguration` class offers the most common parameters to configure the client.
|
||||
The javadoc:org.springframework.data.elasticsearch.client.ClientConfiguration[] class offers the most common parameters to configure the client.
|
||||
In the case this is not enough, the user can add callback functions by using the `withClientConfigurer(ClientConfigurationCallback<?>)` method.
|
||||
|
||||
The following callbacks are provided:
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
[[new-features]]
|
||||
= What's new
|
||||
|
||||
[[new-features.5-4-1]]
|
||||
== New in Spring Data Elasticsearch 5.4.1
|
||||
* Upgrade to Elasticsearch 8.15.5.
|
||||
|
||||
[[new-features.5-4-0]]
|
||||
== New in Spring Data Elasticsearch 5.4
|
||||
|
||||
* Upgrade to Elasticsearch 8.15.3.
|
||||
* Allow to customize the mapped type name for `@InnerField` and `@Field` annotations.
|
||||
* Support for Elasticsearch SQL.
|
||||
* Add support for retrieving request executionDuration.
|
||||
|
||||
[[new-features.5-3-0]]
|
||||
== New in Spring Data Elasticsearch 5.3
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
Spring Data Elasticsearch uses several interfaces to define the operations that can be called against an Elasticsearch index (for a description of the reactive interfaces see xref:elasticsearch/reactive-template.adoc[]).
|
||||
|
||||
* `IndexOperations` defines actions on index level like creating or deleting an index.
|
||||
* `DocumentOperations` defines actions to store, update and retrieve entities based on their id.
|
||||
* `SearchOperations` define the actions to search for multiple entities using queries
|
||||
* `ElasticsearchOperations` combines the `DocumentOperations` and `SearchOperations` interfaces.
|
||||
* javadoc:org.springframework.data.elasticsearch.core.IndexOperations[] defines actions on index level like creating or deleting an index.
|
||||
* javadoc:org.springframework.data.elasticsearch.core.DocumentOperations[] defines actions to store, update and retrieve entities based on their id.
|
||||
* javadoc:org.springframework.data.elasticsearch.core.SearchOperations[] define the actions to search for multiple entities using queries
|
||||
* javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] combines the `DocumentOperations` and `SearchOperations` interfaces.
|
||||
|
||||
These interfaces correspond to the structuring of the https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html[Elasticsearch API].
|
||||
|
||||
@@ -81,7 +81,7 @@ When a document is retrieved with the methods of the `DocumentOperations` inter
|
||||
When searching with the methods of the `SearchOperations` interface, additional information is available for each entity, for example the _score_ or the _sortValues_ of the found entity.
|
||||
|
||||
In order to return this information, each entity is wrapped in a `SearchHit` object that contains this entity-specific additional information.
|
||||
These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations.
|
||||
These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations or the execution duration it took to complete the request.
|
||||
The following classes and interfaces are now available:
|
||||
|
||||
.SearchHit<T>
|
||||
|
||||
@@ -6,10 +6,11 @@ The following table shows the Elasticsearch and Spring versions that are used by
|
||||
[cols="^,^,^,^",options="header"]
|
||||
|===
|
||||
| Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework
|
||||
| 2024.0 (?) | 5.3.x | 8.13.2 | ?
|
||||
| 2023.1 (Vaughan) | 5.2.x | 8.11.1 | 6.1.x
|
||||
| 2023.0 (Ullmann) | 5.1.x | 8.7.1 | 6.0.x
|
||||
| 2022.0 (Turing) | 5.0.xfootnote:oom[Out of maintenance] | 8.5.3 | 6.0.x
|
||||
| 2024.1 | 5.4.x | 8.15.5 | 6.2.x
|
||||
| 2024.0 | 5.3.1 | 8.13.4 | 6.1.x
|
||||
| 2023.1 (Vaughan) | 5.2.xfootnote:oom[Out of maintenance] | 8.11.1 | 6.1.x
|
||||
| 2023.0 (Ullmann) | 5.1.xfootnote:oom[] | 8.7.1 | 6.0.x
|
||||
| 2022.0 (Turing) | 5.0.xfootnote:oom[] | 8.5.3 | 6.0.x
|
||||
| 2021.2 (Raj) | 4.4.xfootnote:oom[] | 7.17.3 | 5.3.x
|
||||
| 2021.1 (Q) | 4.3.xfootnote:oom[] | 7.15.2 | 5.3.x
|
||||
| 2021.0 (Pascal) | 4.2.xfootnote:oom[] | 7.12.0 | 5.3.x
|
||||
|
||||
@@ -6,10 +6,16 @@ This section describes breaking changes from version 5.2.x to 5.3.x and how remo
|
||||
[[elasticsearch-migration-guide-5.2-5.3.breaking-changes]]
|
||||
== Breaking Changes
|
||||
|
||||
During the parameter replacement in `@Query` annotated repository methods previous versions wrote the String `"null"` into the query that was sent to Elasticsearch when the actual parameter value was `null`.
|
||||
As Elasticsearch does not store `null` values, this behaviour could lead to problems, for example whent the fields to be searched contains the string `"null"`.
|
||||
In Version 5.3 a `null` value in a parameter will cause a `ConversionException` to be thrown.
|
||||
If you are using `"null"` as the
|
||||
`null_value` defined in a field mapping, then pass that string into the query instead of a Java `null`.
|
||||
|
||||
[[elasticsearch-migration-guide-5.2-5.3.deprecations]]
|
||||
== Deprecations
|
||||
|
||||
=== Removals
|
||||
|
||||
The deprecated classes `org.springframework.data.elasticsearch.ELCQueries`
|
||||
and `org.springframework.data.elasticsearch.client.elc.QueryBuilders` have been removed, use `org.springframework.data.elasticsearch.client.elc.Queries` instead.
|
||||
and `org.springframework.data.elasticsearch.client.elc.QueryBuilders` have been removed, use `org.springframework.data.elasticsearch.client.elc.Queries` instead.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[[elasticsearch-migration-guide-5.3-5.4]]
|
||||
= Upgrading from 5.3.x to 5.4.x
|
||||
|
||||
This section describes breaking changes from version 5.3.x to 5.4.x and how removed features can be replaced by new introduced features.
|
||||
|
||||
[[elasticsearch-migration-guide-5.3-5.4.breaking-changes]]
|
||||
== Breaking Changes
|
||||
|
||||
[[elasticsearch-migration-guide-5.3-5.4.breaking-changes.knn-search]]
|
||||
=== knn search
|
||||
The `withKnnQuery` method in `NativeQueryBuilder` has been replaced with `withKnnSearches` to build a `NativeQuery` with knn search.
|
||||
|
||||
`KnnQuery` and `KnnSearch` are two different classes in elasticsearch java client and are used for different queries, with different parameters supported:
|
||||
|
||||
- `KnnSearch`: is https://www.elastic.co/guide/en/elasticsearch/reference/8.13/search-search.html#search-api-knn[the top level `knn` query] in the elasticsearch request;
|
||||
- `KnnQuery`: is https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html[the `knn` query inside `query` clause];
|
||||
|
||||
If `KnnQuery` is still preferable, please be sure to construct it inside `query` clause manually, by means of `withQuery(co.elastic.clients.elasticsearch._types.query_dsl.Query query)` clause in `NativeQueryBuilder`.
|
||||
|
||||
[[elasticsearch-migration-guide-5.3-5.4.deprecations]]
|
||||
== Deprecations
|
||||
|
||||
=== Removals
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2024 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 java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Identifies an alias for the index.
|
||||
*
|
||||
* @author Youssef Aouichaoui
|
||||
* @since 5.4
|
||||
*/
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE })
|
||||
@Repeatable(Aliases.class)
|
||||
public @interface Alias {
|
||||
/**
|
||||
* @return Index alias name. Alias for {@link #alias}.
|
||||
*/
|
||||
@AliasFor("alias")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* @return Index alias name. Alias for {@link #value}.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String alias() default "";
|
||||
|
||||
/**
|
||||
* @return Query used to limit documents the alias can access.
|
||||
*/
|
||||
Filter filter() default @Filter;
|
||||
|
||||
/**
|
||||
* @return Used to route indexing operations to a specific shard.
|
||||
*/
|
||||
String indexRouting() default "";
|
||||
|
||||
/**
|
||||
* @return Used to route indexing and search operations to a specific shard.
|
||||
*/
|
||||
String routing() default "";
|
||||
|
||||
/**
|
||||
* @return Used to route search operations to a specific shard.
|
||||
*/
|
||||
String searchRouting() default "";
|
||||
|
||||
/**
|
||||
* @return Is the alias hidden?
|
||||
*/
|
||||
boolean isHidden() default false;
|
||||
|
||||
/**
|
||||
* @return Is it the 'write index' for the alias?
|
||||
*/
|
||||
boolean isWriteIndex() default false;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2024 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 java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Container annotation that aggregates several {@link Alias} annotations.
|
||||
*
|
||||
* @author Youssef Aouichaoui
|
||||
* @see Alias
|
||||
* @since 5.4
|
||||
*/
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE })
|
||||
public @interface Aliases {
|
||||
Alias[] value();
|
||||
}
|
||||
@@ -100,6 +100,13 @@ public @interface Document {
|
||||
*/
|
||||
boolean storeVersionInSource() default true;
|
||||
|
||||
/**
|
||||
* Aliases for the index.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
Alias[] aliases() default {};
|
||||
|
||||
/**
|
||||
* @since 4.3
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,8 @@ import org.springframework.core.annotation.AliasFor;
|
||||
* @author Brian Kimmig
|
||||
* @author Morgan Lutz
|
||||
* @author Sascha Woo
|
||||
* @author Haibo Liu
|
||||
* @author Andriy Redko
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD })
|
||||
@@ -128,6 +130,10 @@ public @interface Field {
|
||||
boolean norms() default true;
|
||||
|
||||
/**
|
||||
* NOte that null_value setting are not supported in Elasticsearch for all types. For example setting a null_value on
|
||||
* a field with type text will throw an exception in the server when the mapping is written to Elasticsearch. Alas,
|
||||
* the Elasticsearch documentation does not specify on which types it is allowed on which it is not.
|
||||
*
|
||||
* @since 4.0
|
||||
*/
|
||||
String nullValue() default "";
|
||||
@@ -195,6 +201,27 @@ public @interface Field {
|
||||
*/
|
||||
int dims() default -1;
|
||||
|
||||
/**
|
||||
* to be used in combination with {@link FieldType#Dense_Vector}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
String elementType() default FieldElementType.DEFAULT;
|
||||
|
||||
/**
|
||||
* to be used in combination with {@link FieldType#Dense_Vector}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
KnnSimilarity knnSimilarity() default KnnSimilarity.DEFAULT;
|
||||
|
||||
/**
|
||||
* to be used in combination with {@link FieldType#Dense_Vector}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
KnnIndexOptions[] knnIndexOptions() default {};
|
||||
|
||||
/**
|
||||
* Controls how Elasticsearch dynamically adds fields to the inner object within the document.<br>
|
||||
* To be used in combination with {@link FieldType#Object} or {@link FieldType#Nested}
|
||||
@@ -218,4 +245,11 @@ public @interface Field {
|
||||
* @since 5.1
|
||||
*/
|
||||
boolean storeEmptyValue() default true;
|
||||
|
||||
/**
|
||||
* overrides the field type in the mapping which otherwise will be taken from corresponding {@link FieldType}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
String mappedTypeName() default "";
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2024 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;
|
||||
|
||||
/**
|
||||
* @author Haibo Liu
|
||||
* @since 5.4
|
||||
*/
|
||||
public final class FieldElementType {
|
||||
public final static String DEFAULT = "";
|
||||
public final static String FLOAT = "float";
|
||||
public final static String BYTE = "byte";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 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.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Query used to limit documents.
|
||||
*
|
||||
* @author Youssef Aouichaoui
|
||||
* @since 5.4
|
||||
*/
|
||||
public @interface Filter {
|
||||
/**
|
||||
* @return Query used to limit documents. Alias for {@link #query}.
|
||||
*/
|
||||
@AliasFor("query")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* @return Query used to limit documents. Alias for {@link #value}.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String query() default "";
|
||||
}
|
||||
@@ -29,6 +29,8 @@ import java.lang.annotation.Target;
|
||||
* @author Aleksei Arsenev
|
||||
* @author Brian Kimmig
|
||||
* @author Morgan Lutz
|
||||
* @author Haibo Liu
|
||||
* @author Andriy Redko
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.ANNOTATION_TYPE)
|
||||
@@ -149,4 +151,32 @@ public @interface InnerField {
|
||||
* @since 4.2
|
||||
*/
|
||||
int dims() default -1;
|
||||
|
||||
/**
|
||||
* to be used in combination with {@link FieldType#Dense_Vector}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
String elementType() default FieldElementType.DEFAULT;
|
||||
|
||||
/**
|
||||
* to be used in combination with {@link FieldType#Dense_Vector}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
KnnSimilarity knnSimilarity() default KnnSimilarity.DEFAULT;
|
||||
|
||||
/**
|
||||
* to be used in combination with {@link FieldType#Dense_Vector}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
KnnIndexOptions[] knnIndexOptions() default {};
|
||||
|
||||
/**
|
||||
* overrides the field type in the mapping which otherwise will be taken from corresponding {@link FieldType}
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
String mappedTypeName() default "";
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 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;
|
||||
|
||||
/**
|
||||
* @author Haibo Liu
|
||||
* @since 5.4
|
||||
*/
|
||||
public enum KnnAlgorithmType {
|
||||
HNSW("hnsw"),
|
||||
INT8_HNSW("int8_hnsw"),
|
||||
FLAT("flat"),
|
||||
INT8_FLAT("int8_flat"),
|
||||
DEFAULT("");
|
||||
|
||||
private final String type;
|
||||
|
||||
KnnAlgorithmType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024 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;
|
||||
|
||||
/**
|
||||
* @author Haibo Liu
|
||||
* @since 5.4
|
||||
*/
|
||||
public @interface KnnIndexOptions {
|
||||
|
||||
KnnAlgorithmType type() default KnnAlgorithmType.DEFAULT;
|
||||
|
||||
/**
|
||||
* Only applicable to {@link KnnAlgorithmType#HNSW} and {@link KnnAlgorithmType#INT8_HNSW} index types.
|
||||
*/
|
||||
int m() default -1;
|
||||
|
||||
/**
|
||||
* Only applicable to {@link KnnAlgorithmType#HNSW} and {@link KnnAlgorithmType#INT8_HNSW} index types.
|
||||
*/
|
||||
int efConstruction() default -1;
|
||||
|
||||
/**
|
||||
* Only applicable to {@link KnnAlgorithmType#INT8_HNSW} and {@link KnnAlgorithmType#INT8_FLAT} index types.
|
||||
*/
|
||||
float confidenceInterval() default -1F;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 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;
|
||||
|
||||
/**
|
||||
* @author Haibo Liu
|
||||
* @since 5.4
|
||||
*/
|
||||
public enum KnnSimilarity {
|
||||
L2_NORM("l2_norm"),
|
||||
DOT_PRODUCT("dot_product"),
|
||||
COSINE("cosine"),
|
||||
MAX_INNER_PRODUCT("max_inner_product"),
|
||||
DEFAULT("");
|
||||
|
||||
private final String similarity;
|
||||
|
||||
KnnSimilarity(String similarity) {
|
||||
this.similarity = similarity;
|
||||
}
|
||||
|
||||
public String getSimilarity() {
|
||||
return similarity;
|
||||
}
|
||||
}
|
||||
+38
-33
@@ -127,7 +127,7 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
|
||||
mustQueries.add(Query.of(qb -> qb.matchAll(m -> m)));
|
||||
}
|
||||
|
||||
return new Query.Builder().bool(boolQueryBuilder -> {
|
||||
return new Query.Builder().bool(boolQueryBuilder -> {
|
||||
|
||||
if (!shouldQueries.isEmpty()) {
|
||||
boolQueryBuilder.should(shouldQueries);
|
||||
@@ -249,49 +249,54 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
|
||||
queryBuilder.queryString(queryStringQuery(fieldName, Objects.requireNonNull(value).toString(), boost));
|
||||
break;
|
||||
case LESS:
|
||||
queryBuilder //
|
||||
.range(rb -> rb //
|
||||
.field(fieldName) //
|
||||
.lt(JsonData.of(value)) //
|
||||
.boost(boost)); //
|
||||
queryBuilder
|
||||
.range(rb -> rb
|
||||
.untyped(ut -> ut
|
||||
.field(fieldName)
|
||||
.lt(JsonData.of(value))
|
||||
.boost(boost)));
|
||||
break;
|
||||
case LESS_EQUAL:
|
||||
queryBuilder //
|
||||
.range(rb -> rb //
|
||||
.field(fieldName) //
|
||||
.lte(JsonData.of(value)) //
|
||||
.boost(boost)); //
|
||||
queryBuilder
|
||||
.range(rb -> rb
|
||||
.untyped(ut -> ut
|
||||
.field(fieldName)
|
||||
.lte(JsonData.of(value))
|
||||
.boost(boost)));
|
||||
break;
|
||||
case GREATER:
|
||||
queryBuilder //
|
||||
.range(rb -> rb //
|
||||
.field(fieldName) //
|
||||
.gt(JsonData.of(value)) //
|
||||
.boost(boost)); //
|
||||
queryBuilder
|
||||
.range(rb -> rb
|
||||
.untyped(ut -> ut
|
||||
.field(fieldName)
|
||||
.gt(JsonData.of(value))
|
||||
.boost(boost)));
|
||||
break;
|
||||
case GREATER_EQUAL:
|
||||
queryBuilder //
|
||||
.range(rb -> rb //
|
||||
.field(fieldName) //
|
||||
.gte(JsonData.of(value)) //
|
||||
.boost(boost)); //
|
||||
queryBuilder
|
||||
.range(rb -> rb
|
||||
.untyped(ut -> ut
|
||||
.field(fieldName)
|
||||
.gte(JsonData.of(value))
|
||||
.boost(boost)));
|
||||
break;
|
||||
case BETWEEN:
|
||||
Object[] ranges = (Object[]) value;
|
||||
Assert.notNull(value, "value for a between condition must not be null");
|
||||
queryBuilder //
|
||||
.range(rb -> {
|
||||
rb.field(fieldName);
|
||||
if (ranges[0] != null) {
|
||||
rb.gte(JsonData.of(ranges[0]));
|
||||
}
|
||||
queryBuilder
|
||||
.range(rb -> rb
|
||||
.untyped(ut -> {
|
||||
ut.field(fieldName);
|
||||
if (ranges[0] != null) {
|
||||
ut.gte(JsonData.of(ranges[0]));
|
||||
}
|
||||
|
||||
if (ranges[1] != null) {
|
||||
rb.lte(JsonData.of(ranges[1]));
|
||||
}
|
||||
rb.boost(boost); //
|
||||
return rb;
|
||||
}); //
|
||||
if (ranges[1] != null) {
|
||||
ut.lte(JsonData.of(ranges[1]));
|
||||
}
|
||||
ut.boost(boost); //
|
||||
return ut;
|
||||
}));
|
||||
|
||||
break;
|
||||
case FUZZY:
|
||||
|
||||
+2
-1
@@ -50,6 +50,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.4
|
||||
*/
|
||||
final class DocumentAdapters {
|
||||
@@ -74,7 +75,7 @@ final class DocumentAdapters {
|
||||
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
|
||||
hit.innerHits().forEach((name, innerHitsResult) -> {
|
||||
// noinspection ReturnOfNull
|
||||
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, null, null,
|
||||
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null,
|
||||
searchDocument -> null, jsonpMapper));
|
||||
});
|
||||
|
||||
|
||||
+19
@@ -23,6 +23,8 @@ import co.elastic.clients.elasticsearch.core.*;
|
||||
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
|
||||
import co.elastic.clients.elasticsearch.core.msearch.MultiSearchResponseItem;
|
||||
import co.elastic.clients.elasticsearch.core.search.ResponseBody;
|
||||
import co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient;
|
||||
import co.elastic.clients.elasticsearch.sql.QueryResponse;
|
||||
import co.elastic.clients.json.JsonpMapper;
|
||||
import co.elastic.clients.transport.Version;
|
||||
|
||||
@@ -56,6 +58,7 @@ import org.springframework.data.elasticsearch.core.query.UpdateResponse;
|
||||
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
|
||||
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
|
||||
import org.springframework.data.elasticsearch.core.script.Script;
|
||||
import org.springframework.data.elasticsearch.core.sql.SqlResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -74,6 +77,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
private static final Log LOGGER = LogFactory.getLog(ElasticsearchTemplate.class);
|
||||
|
||||
private final ElasticsearchClient client;
|
||||
private final ElasticsearchSqlClient sqlClient;
|
||||
private final RequestConverter requestConverter;
|
||||
private final ResponseConverter responseConverter;
|
||||
private final JsonpMapper jsonpMapper;
|
||||
@@ -85,6 +89,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
Assert.notNull(client, "client must not be null");
|
||||
|
||||
this.client = client;
|
||||
this.sqlClient = client.sql();
|
||||
this.jsonpMapper = client._transport().jsonpMapper();
|
||||
requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper);
|
||||
responseConverter = new ResponseConverter(jsonpMapper);
|
||||
@@ -97,6 +102,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
Assert.notNull(client, "client must not be null");
|
||||
|
||||
this.client = client;
|
||||
this.sqlClient = client.sql();
|
||||
this.jsonpMapper = client._transport().jsonpMapper();
|
||||
requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper);
|
||||
responseConverter = new ResponseConverter(jsonpMapper);
|
||||
@@ -656,6 +662,19 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
DeleteScriptRequest request = requestConverter.scriptDelete(name);
|
||||
return execute(client -> client.deleteScript(request)).acknowledged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlResponse search(SqlQuery query) {
|
||||
Assert.notNull(query, "Query must not be null.");
|
||||
|
||||
try {
|
||||
QueryResponse response = sqlClient.query(requestConverter.sqlQueryRequest(query));
|
||||
|
||||
return responseConverter.sqlResponse(response);
|
||||
} catch (IOException e) {
|
||||
throw exceptionTranslator.translateException(e);
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region client callback
|
||||
|
||||
+21
-11
@@ -21,13 +21,12 @@ import co.elastic.clients.elasticsearch.indices.*;
|
||||
import co.elastic.clients.transport.ElasticsearchTransport;
|
||||
import co.elastic.clients.transport.endpoints.BooleanResponse;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
|
||||
@@ -46,6 +45,8 @@ import org.springframework.data.elasticsearch.core.index.GetIndexTemplateRequest
|
||||
import org.springframework.data.elasticsearch.core.index.GetTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.index.PutIndexTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.index.PutTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.mapping.Alias;
|
||||
import org.springframework.data.elasticsearch.core.mapping.CreateIndexSettings;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -60,8 +61,6 @@ import org.springframework.util.Assert;
|
||||
public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, ElasticsearchIndicesClient>
|
||||
implements IndexOperations {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(IndicesTemplate.class);
|
||||
|
||||
// we need a cluster client as well because ES has put some methods from the indices API into the cluster client
|
||||
// (component templates)
|
||||
private final ClusterTemplate clusterTemplate;
|
||||
@@ -85,7 +84,7 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
|
||||
}
|
||||
|
||||
public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate clusterTemplate,
|
||||
ElasticsearchConverter elasticsearchConverter, IndexCoordinates boundIndex) {
|
||||
ElasticsearchConverter elasticsearchConverter, IndexCoordinates boundIndex) {
|
||||
super(client, elasticsearchConverter);
|
||||
|
||||
Assert.notNull(clusterTemplate, "cluster must not be null");
|
||||
@@ -137,11 +136,14 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
|
||||
|
||||
protected boolean doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
|
||||
@Nullable Document mapping) {
|
||||
Set<Alias> aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>();
|
||||
CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates)
|
||||
.withAliases(aliases)
|
||||
.withSettings(settings)
|
||||
.withMapping(mapping)
|
||||
.build();
|
||||
|
||||
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
|
||||
Assert.notNull(settings, "settings must not be null");
|
||||
|
||||
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping);
|
||||
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings);
|
||||
CreateIndexResponse createIndexResponse = execute(client -> client.create(createIndexRequest));
|
||||
return Boolean.TRUE.equals(createIndexResponse.acknowledged());
|
||||
}
|
||||
@@ -236,8 +238,7 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
|
||||
GetMappingRequest getMappingRequest = requestConverter.indicesGetMappingRequest(indexCoordinates);
|
||||
GetMappingResponse getMappingResponse = execute(client -> client.getMapping(getMappingRequest));
|
||||
|
||||
Document mappingResponse = responseConverter.indicesGetMapping(getMappingResponse, indexCoordinates);
|
||||
return mappingResponse;
|
||||
return responseConverter.indicesGetMapping(getMappingResponse, indexCoordinates);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -449,5 +450,14 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
|
||||
public IndexCoordinates getIndexCoordinatesFor(Class<?> clazz) {
|
||||
return getRequiredPersistentEntity(clazz).getIndexCoordinates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Alias} of the provided class.
|
||||
*
|
||||
* @param clazz provided class that can be used to extract aliases.
|
||||
*/
|
||||
public Set<Alias> getAliasesFor(Class<?> clazz) {
|
||||
return getRequiredPersistentEntity(clazz).getAliases();
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.client.elc;
|
||||
|
||||
import co.elastic.clients.elasticsearch._types.KnnQuery;
|
||||
import co.elastic.clients.elasticsearch._types.KnnSearch;
|
||||
import co.elastic.clients.elasticsearch._types.SortOptions;
|
||||
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
|
||||
@@ -29,7 +29,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.query.BaseQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.ScriptedField;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -39,6 +38,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Sascha Woo
|
||||
* @author Haibo Liu
|
||||
* @since 4.4
|
||||
*/
|
||||
public class NativeQuery extends BaseQuery {
|
||||
@@ -53,7 +53,7 @@ public class NativeQuery extends BaseQuery {
|
||||
private List<SortOptions> sortOptions = Collections.emptyList();
|
||||
|
||||
private Map<String, JsonData> searchExtensions = Collections.emptyMap();
|
||||
@Nullable private KnnQuery knnQuery;
|
||||
@Nullable private List<KnnSearch> knnSearches = Collections.emptyList();
|
||||
|
||||
public NativeQuery(NativeQueryBuilder builder) {
|
||||
super(builder);
|
||||
@@ -70,7 +70,7 @@ public class NativeQuery extends BaseQuery {
|
||||
"Cannot add an NativeQuery in a NativeQuery");
|
||||
}
|
||||
this.springDataQuery = builder.getSpringDataQuery();
|
||||
this.knnQuery = builder.getKnnQuery();
|
||||
this.knnSearches = builder.getKnnSearches();
|
||||
}
|
||||
|
||||
public NativeQuery(@Nullable Query query) {
|
||||
@@ -122,11 +122,11 @@ public class NativeQuery extends BaseQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.1
|
||||
* @since 5.3.1
|
||||
*/
|
||||
@Nullable
|
||||
public KnnQuery getKnnQuery() {
|
||||
return knnQuery;
|
||||
public List<KnnSearch> getKnnSearches() {
|
||||
return knnSearches;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
+32
-3
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.elasticsearch.client.elc;
|
||||
|
||||
import co.elastic.clients.elasticsearch._types.KnnQuery;
|
||||
import co.elastic.clients.elasticsearch._types.KnnSearch;
|
||||
import co.elastic.clients.elasticsearch._types.SortOptions;
|
||||
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
|
||||
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
|
||||
@@ -26,6 +27,7 @@ import co.elastic.clients.util.ObjectBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -38,6 +40,7 @@ import org.springframework.util.Assert;
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Sascha Woo
|
||||
* @author Haibo Liu
|
||||
* @since 4.4
|
||||
*/
|
||||
public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQueryBuilder> {
|
||||
@@ -52,6 +55,7 @@ public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQuer
|
||||
|
||||
@Nullable private org.springframework.data.elasticsearch.core.query.Query springDataQuery;
|
||||
@Nullable private KnnQuery knnQuery;
|
||||
@Nullable private List<KnnSearch> knnSearches = Collections.emptyList();
|
||||
|
||||
public NativeQueryBuilder() {}
|
||||
|
||||
@@ -92,6 +96,14 @@ public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQuer
|
||||
return knnQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.3.1
|
||||
*/
|
||||
@Nullable
|
||||
public List<KnnSearch> getKnnSearches() {
|
||||
return knnSearches;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public org.springframework.data.elasticsearch.core.query.Query getSpringDataQuery() {
|
||||
return springDataQuery;
|
||||
@@ -202,13 +214,30 @@ public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQuer
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.1
|
||||
* @since 5.4
|
||||
*/
|
||||
public NativeQueryBuilder withKnnQuery(KnnQuery knnQuery) {
|
||||
this.knnQuery = knnQuery;
|
||||
public NativeQueryBuilder withKnnSearches(List<KnnSearch> knnSearches) {
|
||||
this.knnSearches = knnSearches;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.4
|
||||
*/
|
||||
public NativeQueryBuilder withKnnSearches(Function<KnnSearch.Builder, ObjectBuilder<KnnSearch>> fn) {
|
||||
|
||||
Assert.notNull(fn, "fn must not be null");
|
||||
|
||||
return withKnnSearches(fn.apply(new KnnSearch.Builder()).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.4
|
||||
*/
|
||||
public NativeQueryBuilder withKnnSearches(KnnSearch knnSearch) {
|
||||
return withKnnSearches(List.of(knnSearch));
|
||||
}
|
||||
|
||||
public NativeQuery build() {
|
||||
Assert.isTrue(query == null || springDataQuery == null, "Cannot have both a native query and a Spring Data query");
|
||||
return new NativeQuery(this);
|
||||
|
||||
+29
-4
@@ -36,6 +36,7 @@ import org.springframework.util.Assert;
|
||||
* Reactive version of {@link co.elastic.clients.elasticsearch.ElasticsearchClient}.
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
* @author maryantocinn
|
||||
* @since 4.4
|
||||
*/
|
||||
public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTransport, ReactiveElasticsearchClient>
|
||||
@@ -69,6 +70,10 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
|
||||
return new ReactiveElasticsearchIndicesClient(transport, transportOptions);
|
||||
}
|
||||
|
||||
public ReactiveElasticsearchSqlClient sql() {
|
||||
return new ReactiveElasticsearchSqlClient(transport, transportOptions);
|
||||
}
|
||||
|
||||
// endregion
|
||||
// region info
|
||||
|
||||
@@ -122,7 +127,7 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
|
||||
// java.lang.Class<TDocument>)
|
||||
// noinspection unchecked
|
||||
JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse>) GetRequest._ENDPOINT;
|
||||
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.get.TDocument",
|
||||
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.get.Response.TDocument",
|
||||
getDeserializer(tClass));
|
||||
|
||||
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
|
||||
@@ -141,7 +146,7 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
|
||||
|
||||
// noinspection unchecked
|
||||
JsonEndpoint<UpdateRequest<?, ?>, UpdateResponse<T>, ErrorResponse> endpoint = new EndpointWithResponseMapperAttr(
|
||||
UpdateRequest._ENDPOINT, "co.elastic.clients:Deserializer:_global.update.TDocument",
|
||||
UpdateRequest._ENDPOINT, "co.elastic.clients:Deserializer:_global.update.Response.TDocument",
|
||||
this.getDeserializer(clazz));
|
||||
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, this.transportOptions));
|
||||
}
|
||||
@@ -167,7 +172,7 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
|
||||
|
||||
// noinspection unchecked
|
||||
JsonEndpoint<MgetRequest, MgetResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<MgetRequest, MgetResponse<T>, ErrorResponse>) MgetRequest._ENDPOINT;
|
||||
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.mget.TDocument",
|
||||
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.mget.Response.TDocument",
|
||||
this.getDeserializer(clazz));
|
||||
|
||||
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
|
||||
@@ -223,6 +228,26 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
|
||||
return deleteByQuery(fn.apply(new DeleteByQueryRequest.Builder()).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.4
|
||||
*/
|
||||
public Mono<CountResponse> count(CountRequest request) {
|
||||
|
||||
Assert.notNull(request, "request must not be null");
|
||||
|
||||
return Mono.fromFuture(transport.performRequestAsync(request, CountRequest._ENDPOINT, transportOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.4
|
||||
*/
|
||||
public Mono<CountResponse> count(Function<CountRequest.Builder, ObjectBuilder<CountRequest>> fn) {
|
||||
|
||||
Assert.notNull(fn, "fn must not be null");
|
||||
|
||||
return count(fn.apply(new CountRequest.Builder()).build());
|
||||
}
|
||||
|
||||
// endregion
|
||||
// region search
|
||||
|
||||
@@ -278,7 +303,7 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
|
||||
// noinspection unchecked
|
||||
JsonEndpoint<ScrollRequest, ScrollResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<ScrollRequest, ScrollResponse<T>, ErrorResponse>) ScrollRequest._ENDPOINT;
|
||||
endpoint = new EndpointWithResponseMapperAttr<>(endpoint,
|
||||
"co.elastic.clients:Deserializer:_global.scroll.TDocument", getDeserializer(tDocumentClass));
|
||||
"co.elastic.clients:Deserializer:_global.scroll.Response.TDocument", getDeserializer(tDocumentClass));
|
||||
|
||||
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
|
||||
}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.client.elc;
|
||||
|
||||
import co.elastic.clients.ApiClient;
|
||||
import co.elastic.clients.elasticsearch._types.ElasticsearchException;
|
||||
import co.elastic.clients.elasticsearch.sql.QueryRequest;
|
||||
import co.elastic.clients.elasticsearch.sql.QueryResponse;
|
||||
import co.elastic.clients.transport.ElasticsearchTransport;
|
||||
import co.elastic.clients.transport.TransportOptions;
|
||||
import co.elastic.clients.util.ObjectBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Reactive version of {@link co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient}.
|
||||
*
|
||||
* @author Aouichaoui Youssef
|
||||
* @since 5.4
|
||||
*/
|
||||
public class ReactiveElasticsearchSqlClient extends ApiClient<ElasticsearchTransport, ReactiveElasticsearchSqlClient> {
|
||||
public ReactiveElasticsearchSqlClient(ElasticsearchTransport transport, @Nullable TransportOptions transportOptions) {
|
||||
super(transport, transportOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveElasticsearchSqlClient withTransportOptions(@Nullable TransportOptions transportOptions) {
|
||||
return new ReactiveElasticsearchSqlClient(transport, transportOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a SQL request
|
||||
*
|
||||
* @param fn a function that initializes a builder to create the {@link QueryRequest}.
|
||||
*/
|
||||
public final Mono<QueryResponse> query(Function<QueryRequest.Builder, ObjectBuilder<QueryRequest>> fn)
|
||||
throws IOException, ElasticsearchException {
|
||||
return query(fn.apply(new QueryRequest.Builder()).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a SQL request.
|
||||
*/
|
||||
public Mono<QueryResponse> query(QueryRequest query) {
|
||||
return Mono.fromFuture(transport.performRequestAsync(query, QueryRequest._ENDPOINT, transportOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a SQL request.
|
||||
*/
|
||||
public Mono<QueryResponse> query() {
|
||||
return Mono.fromFuture(
|
||||
transport.performRequestAsync(new QueryRequest.Builder().build(), QueryRequest._ENDPOINT, transportOptions));
|
||||
}
|
||||
}
|
||||
+34
-9
@@ -57,18 +57,12 @@ import org.springframework.data.elasticsearch.core.document.Document;
|
||||
import org.springframework.data.elasticsearch.core.document.SearchDocument;
|
||||
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.core.query.BaseQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder;
|
||||
import org.springframework.data.elasticsearch.core.query.BulkOptions;
|
||||
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
|
||||
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.Query;
|
||||
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.*;
|
||||
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
|
||||
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
|
||||
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
|
||||
import org.springframework.data.elasticsearch.core.script.Script;
|
||||
import org.springframework.data.elasticsearch.core.sql.SqlResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -88,6 +82,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
|
||||
private static final Log LOGGER = LogFactory.getLog(ReactiveElasticsearchTemplate.class);
|
||||
|
||||
private final ReactiveElasticsearchClient client;
|
||||
private final ReactiveElasticsearchSqlClient sqlClient;
|
||||
private final RequestConverter requestConverter;
|
||||
private final ResponseConverter responseConverter;
|
||||
private final JsonpMapper jsonpMapper;
|
||||
@@ -99,6 +94,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
|
||||
Assert.notNull(client, "client must not be null");
|
||||
|
||||
this.client = client;
|
||||
this.sqlClient = client.sql();
|
||||
this.jsonpMapper = client._transport().jsonpMapper();
|
||||
requestConverter = new RequestConverter(converter, jsonpMapper);
|
||||
responseConverter = new ResponseConverter(jsonpMapper);
|
||||
@@ -395,7 +391,28 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
|
||||
Function<PitSearchAfter, Publisher<? extends ResponseBody<EntityAsMap>>> resourceClosure = psa -> {
|
||||
|
||||
baseQuery.setPointInTime(new Query.PointInTime(psa.getPit(), pitKeepAlive));
|
||||
baseQuery.addSort(Sort.by("_shard_doc"));
|
||||
|
||||
// only add _shard_doc if there is not a field_collapse and a sort with the same name
|
||||
boolean addShardDoc = true;
|
||||
|
||||
if (query instanceof NativeQuery nativeQuery && nativeQuery.getFieldCollapse() != null) {
|
||||
var field = nativeQuery.getFieldCollapse().field();
|
||||
|
||||
if (nativeQuery.getSortOptions().stream()
|
||||
.anyMatch(sortOptions -> sortOptions.isField() && sortOptions.field().field().equals(field))) {
|
||||
addShardDoc = false;
|
||||
}
|
||||
|
||||
if (query.getSort() != null
|
||||
&& query.getSort().stream().anyMatch(order -> order.getProperty().equals(field))) {
|
||||
addShardDoc = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (addShardDoc) {
|
||||
baseQuery.addSort(Sort.by("_shard_doc"));
|
||||
}
|
||||
|
||||
SearchRequest firstSearchRequest = requestConverter.searchRequest(baseQuery, routingResolver.getRouting(),
|
||||
clazz, index, false, true);
|
||||
|
||||
@@ -625,6 +642,14 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
|
||||
return NativeQuery.builder().withIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SqlResponse> search(SqlQuery query) {
|
||||
Assert.notNull(query, "Query must not be null.");
|
||||
|
||||
co.elastic.clients.elasticsearch.sql.QueryRequest request = requestConverter.sqlQueryRequest(query);
|
||||
return sqlClient.query(request).onErrorMap(this::translateException).map(responseConverter::sqlResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface to be used with {@link #execute(ReactiveElasticsearchTemplate.ClientCallback<>)} for operating
|
||||
* directly on {@link ReactiveElasticsearchClient}.
|
||||
|
||||
+20
-2
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.client.elc;
|
||||
|
||||
import static org.springframework.util.StringUtils.hasText;
|
||||
import static org.springframework.util.StringUtils.*;
|
||||
|
||||
import co.elastic.clients.elasticsearch._types.AcknowledgedResponseBase;
|
||||
import co.elastic.clients.elasticsearch.indices.*;
|
||||
@@ -24,6 +24,7 @@ import co.elastic.clients.transport.endpoints.BooleanResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -46,6 +47,8 @@ import org.springframework.data.elasticsearch.core.index.GetIndexTemplateRequest
|
||||
import org.springframework.data.elasticsearch.core.index.GetTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.index.PutIndexTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.index.PutTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.mapping.Alias;
|
||||
import org.springframework.data.elasticsearch.core.mapping.CreateIndexSettings;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -130,8 +133,14 @@ public class ReactiveIndicesTemplate
|
||||
|
||||
private Mono<Boolean> doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
|
||||
@Nullable Document mapping) {
|
||||
Set<Alias> aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>();
|
||||
CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates)
|
||||
.withAliases(aliases)
|
||||
.withSettings(settings)
|
||||
.withMapping(mapping)
|
||||
.build();
|
||||
|
||||
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping);
|
||||
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings);
|
||||
Mono<CreateIndexResponse> createIndexResponse = Mono.from(execute(client -> client.create(createIndexRequest)));
|
||||
return createIndexResponse.map(CreateIndexResponse::acknowledged);
|
||||
}
|
||||
@@ -435,6 +444,15 @@ public class ReactiveIndicesTemplate
|
||||
return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getIndexCoordinates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Alias} of the provided class.
|
||||
*
|
||||
* @param clazz provided class that can be used to extract aliases.
|
||||
*/
|
||||
private Set<Alias> getAliasesFor(Class<?> clazz) {
|
||||
return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getAliases();
|
||||
}
|
||||
|
||||
private Class<?> checkForBoundClass() {
|
||||
if (boundClass == null) {
|
||||
throw new InvalidDataAccessApiUsageException("IndexOperations are not bound");
|
||||
|
||||
+86
-61
@@ -20,7 +20,6 @@ import static org.springframework.util.CollectionUtils.*;
|
||||
|
||||
import co.elastic.clients.elasticsearch._types.Conflicts;
|
||||
import co.elastic.clients.elasticsearch._types.ExpandWildcard;
|
||||
import co.elastic.clients.elasticsearch._types.InlineScript;
|
||||
import co.elastic.clients.elasticsearch._types.NestedSortValue;
|
||||
import co.elastic.clients.elasticsearch._types.OpType;
|
||||
import co.elastic.clients.elasticsearch._types.SortOptions;
|
||||
@@ -68,6 +67,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -88,6 +88,8 @@ import org.springframework.data.elasticsearch.core.index.GetIndexTemplateRequest
|
||||
import org.springframework.data.elasticsearch.core.index.GetTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.index.PutIndexTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.index.PutTemplateRequest;
|
||||
import org.springframework.data.elasticsearch.core.mapping.Alias;
|
||||
import org.springframework.data.elasticsearch.core.mapping.CreateIndexSettings;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
@@ -111,7 +113,6 @@ import org.springframework.util.StringUtils;
|
||||
* @author Haibo Liu
|
||||
* @since 4.4
|
||||
*/
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
class RequestConverter extends AbstractQueryProcessor {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
|
||||
@@ -170,7 +171,8 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
}));
|
||||
}
|
||||
|
||||
private Alias.Builder buildAlias(AliasActionParameters parameters, Alias.Builder aliasBuilder) {
|
||||
private co.elastic.clients.elasticsearch.indices.Alias.Builder buildAlias(AliasActionParameters parameters,
|
||||
co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder) {
|
||||
|
||||
if (parameters.getRouting() != null) {
|
||||
aliasBuilder.routing(parameters.getRouting());
|
||||
@@ -234,17 +236,25 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
return new ExistsRequest.Builder().index(Arrays.asList(indexCoordinates.getIndexNames())).build();
|
||||
}
|
||||
|
||||
public CreateIndexRequest indicesCreateRequest(IndexCoordinates indexCoordinates, Map<String, Object> settings,
|
||||
@Nullable Document mapping) {
|
||||
|
||||
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
|
||||
Assert.notNull(settings, "settings must not be null");
|
||||
public CreateIndexRequest indicesCreateRequest(CreateIndexSettings indexSettings) {
|
||||
Map<String, co.elastic.clients.elasticsearch.indices.Alias> aliases = new HashMap<>();
|
||||
for (Alias alias : indexSettings.getAliases()) {
|
||||
co.elastic.clients.elasticsearch.indices.Alias esAlias = co.elastic.clients.elasticsearch.indices.Alias
|
||||
.of(ab -> ab.filter(getQuery(alias.getFilter(), null))
|
||||
.routing(alias.getRouting())
|
||||
.indexRouting(alias.getIndexRouting())
|
||||
.searchRouting(alias.getSearchRouting())
|
||||
.isHidden(alias.getHidden())
|
||||
.isWriteIndex(alias.getWriteIndex()));
|
||||
aliases.put(alias.getAlias(), esAlias);
|
||||
}
|
||||
|
||||
// note: the new client does not support the index.storeType anymore
|
||||
return new CreateIndexRequest.Builder() //
|
||||
.index(indexCoordinates.getIndexName()) //
|
||||
.settings(indexSettings(settings)) //
|
||||
.mappings(typeMapping(mapping)) //
|
||||
.index(indexSettings.getIndexCoordinates().getIndexName()) //
|
||||
.aliases(aliases)
|
||||
.settings(indexSettings(indexSettings.getSettings())) //
|
||||
.mappings(typeMapping(indexSettings.getMapping())) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -399,7 +409,7 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
|
||||
if (putTemplateRequest.getSettings() != null) {
|
||||
Map<String, JsonData> settings = getTemplateParams(putTemplateRequest.getSettings().entrySet());
|
||||
builder.settings(settings);
|
||||
builder.settings(sb -> sb.otherSettings(settings));
|
||||
}
|
||||
|
||||
if (putTemplateRequest.getMappings() != null) {
|
||||
@@ -520,6 +530,22 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
.of(gtr -> gtr.name(getTemplateRequest.getTemplateName()).flatSettings(true));
|
||||
}
|
||||
|
||||
public co.elastic.clients.elasticsearch.sql.QueryRequest sqlQueryRequest(SqlQuery query) {
|
||||
Assert.notNull(query, "Query must not be null.");
|
||||
|
||||
return co.elastic.clients.elasticsearch.sql.QueryRequest.of(sqb -> {
|
||||
sqb.query(query.getQuery()).catalog(query.getCatalog()).columnar(query.getColumnar()).cursor(query.getCursor())
|
||||
.fetchSize(query.getFetchSize()).fieldMultiValueLeniency(query.getFieldMultiValueLeniency())
|
||||
.indexUsingFrozen(query.getIndexIncludeFrozen()).keepAlive(time(query.getKeepAlive()))
|
||||
.keepOnCompletion(query.getKeepOnCompletion()).pageTimeout(time(query.getPageTimeout()))
|
||||
.requestTimeout(time(query.getRequestTimeout()))
|
||||
.waitForCompletionTimeout(time(query.getWaitForCompletionTimeout())).filter(getQuery(query.getFilter(), null))
|
||||
.timeZone(Objects.toString(query.getTimeZone(), null)).format("json");
|
||||
|
||||
return sqb;
|
||||
});
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region documents
|
||||
@@ -717,16 +743,12 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
scriptData.params().forEach((key, value) -> params.put(key, JsonData.of(value, jsonpMapper)));
|
||||
}
|
||||
return co.elastic.clients.elasticsearch._types.Script.of(sb -> {
|
||||
sb.lang(scriptData.language())
|
||||
.params(params);
|
||||
if (scriptData.type() == ScriptType.INLINE) {
|
||||
sb.inline(is -> is //
|
||||
.lang(scriptData.language()) //
|
||||
.source(scriptData.script()) //
|
||||
.params(params)); //
|
||||
sb.source(scriptData.script());
|
||||
} else if (scriptData.type() == ScriptType.STORED) {
|
||||
sb.stored(ss -> ss //
|
||||
.id(scriptData.script()) //
|
||||
.params(params) //
|
||||
);
|
||||
sb.id(scriptData.script());
|
||||
}
|
||||
return sb;
|
||||
});
|
||||
@@ -898,7 +920,9 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
|
||||
ReindexRequest.Script script = reindexRequest.getScript();
|
||||
if (script != null) {
|
||||
builder.script(s -> s.inline(InlineScript.of(i -> i.lang(script.getLang()).source(script.getSource()))));
|
||||
builder.script(sb -> sb
|
||||
.lang(script.getLang())
|
||||
.source(script.getSource()));
|
||||
}
|
||||
|
||||
builder.timeout(time(reindexRequest.getTimeout())) //
|
||||
@@ -1016,11 +1040,14 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
order = sortField.order().jsonValue();
|
||||
}
|
||||
|
||||
return sortField.field() + ":" + order;
|
||||
return sortField.field() + ':' + order;
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
if (query.getRefresh() != null) {
|
||||
dqb.refresh(query.getRefresh());
|
||||
}
|
||||
dqb.allowNoIndices(query.getAllowNoIndices())
|
||||
.conflicts(conflicts(query.getConflicts()))
|
||||
.ignoreUnavailable(query.getIgnoreUnavailable())
|
||||
@@ -1051,21 +1078,15 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
}
|
||||
|
||||
uqb.script(sb -> {
|
||||
sb.lang(query.getLang()).params(params);
|
||||
|
||||
if (query.getScriptType() == ScriptType.INLINE) {
|
||||
sb.inline(is -> is //
|
||||
.lang(query.getLang()) //
|
||||
.source(query.getScript()) //
|
||||
.params(params)); //
|
||||
sb.source(query.getScript()); //
|
||||
} else if (query.getScriptType() == ScriptType.STORED) {
|
||||
sb.stored(ss -> ss //
|
||||
.id(query.getScript()) //
|
||||
.params(params) //
|
||||
);
|
||||
sb.id(query.getScript());
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
uqb //
|
||||
@@ -1320,17 +1341,16 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
String script = runtimeField.getScript();
|
||||
|
||||
if (script != null) {
|
||||
rfb
|
||||
.script(s -> s
|
||||
.inline(is -> {
|
||||
is.source(script);
|
||||
rfb.script(s -> {
|
||||
s.source(script);
|
||||
|
||||
if (runtimeField.getParams() != null) {
|
||||
is.params(TypeUtils.paramsMap(runtimeField.getParams()));
|
||||
}
|
||||
return is;
|
||||
}));
|
||||
if (runtimeField.getParams() != null) {
|
||||
s.params(TypeUtils.paramsMap(runtimeField.getParams()));
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
return rfb;
|
||||
});
|
||||
runtimeMappings.put(runtimeField.getName(), esRuntimeField);
|
||||
@@ -1367,7 +1387,7 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
private Function<MultisearchHeader.Builder, ObjectBuilder<MultisearchHeader>> msearchHeaderBuilder(Query query,
|
||||
IndexCoordinates index, @Nullable String routing) {
|
||||
return h -> {
|
||||
var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null
|
||||
var searchType = (query instanceof NativeQuery nativeQuery && !isEmpty(nativeQuery.getKnnSearches())) ? null
|
||||
: searchType(query.getSearchType());
|
||||
|
||||
h //
|
||||
@@ -1399,7 +1419,7 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
|
||||
ElasticsearchPersistentEntity<?> persistentEntity = getPersistentEntity(clazz);
|
||||
|
||||
var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null
|
||||
var searchType = (query instanceof NativeQuery nativeQuery && !isEmpty(nativeQuery.getKnnSearches())) ? null
|
||||
: searchType(query.getSearchType());
|
||||
|
||||
builder //
|
||||
@@ -1477,8 +1497,8 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
if (query instanceof NativeQuery nativeQuery) {
|
||||
prepareNativeSearch(nativeQuery, builder);
|
||||
}
|
||||
// query.getSort() must be checked after prepareNativeSearch as this already might hav a sort set that must have
|
||||
// higher priority
|
||||
// query.getSort() must be checked after prepareNativeSearch as this already might have a sort set
|
||||
// that must have higher priority
|
||||
if (query.getSort() != null) {
|
||||
List<SortOptions> sortOptions = getSortOptions(query.getSort(), persistentEntity);
|
||||
|
||||
@@ -1500,7 +1520,15 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
}
|
||||
|
||||
if (!isEmpty(query.getSearchAfter())) {
|
||||
builder.searchAfter(query.getSearchAfter().stream().map(TypeUtils::toFieldValue).toList());
|
||||
var fieldValues = query.getSearchAfter().stream().map(TypeUtils::toFieldValue).toList();
|
||||
|
||||
// when there is a field collapse on a native query, and we have a search_after, then the search_after
|
||||
// must only have one entry
|
||||
if (query instanceof NativeQuery nativeQuery && nativeQuery.getFieldCollapse() != null) {
|
||||
builder.searchAfter(fieldValues.get(0));
|
||||
} else {
|
||||
builder.searchAfter(fieldValues);
|
||||
}
|
||||
}
|
||||
|
||||
query.getRescorerQueries().forEach(rescorerQuery -> builder.rescore(getRescore(rescorerQuery)));
|
||||
@@ -1513,16 +1541,14 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
rfb.type(RuntimeFieldType._DESERIALIZER.parse(runtimeField.getType()));
|
||||
String script = runtimeField.getScript();
|
||||
if (script != null) {
|
||||
rfb
|
||||
.script(s -> s
|
||||
.inline(is -> {
|
||||
is.source(script);
|
||||
rfb.script(s -> {
|
||||
s.source(script);
|
||||
|
||||
if (runtimeField.getParams() != null) {
|
||||
is.params(TypeUtils.paramsMap(runtimeField.getParams()));
|
||||
}
|
||||
return is;
|
||||
}));
|
||||
if (runtimeField.getParams() != null) {
|
||||
s.params(TypeUtils.paramsMap(runtimeField.getParams()));
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
return rfb;
|
||||
@@ -1718,8 +1744,8 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
.sort(query.getSortOptions()) //
|
||||
;
|
||||
|
||||
if (query.getKnnQuery() != null) {
|
||||
builder.knn(query.getKnnQuery());
|
||||
if (!isEmpty(query.getKnnSearches())) {
|
||||
builder.knn(query.getKnnSearches());
|
||||
}
|
||||
|
||||
if (!isEmpty(query.getAggregations())) {
|
||||
@@ -1739,8 +1765,8 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
.collapse(query.getFieldCollapse()) //
|
||||
.sort(query.getSortOptions());
|
||||
|
||||
if (query.getKnnQuery() != null) {
|
||||
builder.knn(query.getKnnQuery());
|
||||
if (!isEmpty(query.getKnnSearches())) {
|
||||
builder.knn(query.getKnnSearches());
|
||||
}
|
||||
|
||||
if (!isEmpty(query.getAggregations())) {
|
||||
@@ -1758,7 +1784,6 @@ class RequestConverter extends AbstractQueryProcessor {
|
||||
return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz));
|
||||
}
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private void addPostFilter(Query query, SearchRequest.Builder builder) {
|
||||
|
||||
// we only need to handle NativeQuery here. filter from a CriteriaQuery are added into the query and not as post
|
||||
|
||||
+26
@@ -33,6 +33,8 @@ import co.elastic.clients.elasticsearch.core.mget.MultiGetResponseItem;
|
||||
import co.elastic.clients.elasticsearch.indices.*;
|
||||
import co.elastic.clients.elasticsearch.indices.get_index_template.IndexTemplateItem;
|
||||
import co.elastic.clients.elasticsearch.indices.get_mapping.IndexMappingRecord;
|
||||
import co.elastic.clients.elasticsearch.sql.QueryResponse;
|
||||
import co.elastic.clients.json.JsonData;
|
||||
import co.elastic.clients.json.JsonpMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -61,6 +63,7 @@ import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
|
||||
import org.springframework.data.elasticsearch.core.script.Script;
|
||||
import org.springframework.data.elasticsearch.core.sql.SqlResponse;
|
||||
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -536,6 +539,29 @@ class ResponseConverter {
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region sql
|
||||
public SqlResponse sqlResponse(QueryResponse response) {
|
||||
SqlResponse.Builder builder = SqlResponse.builder();
|
||||
builder.withRunning(Boolean.TRUE.equals(response.isRunning()))
|
||||
.withPartial(Boolean.TRUE.equals(response.isPartial())).withCursor(response.cursor());
|
||||
|
||||
final List<SqlResponse.Column> columns = response.columns().stream()
|
||||
.map(column -> new SqlResponse.Column(column.name(), column.type())).toList();
|
||||
builder.withColumns(columns);
|
||||
|
||||
for (List<JsonData> rowValues : response.rows()) {
|
||||
SqlResponse.Row.Builder rowBuilder = SqlResponse.Row.builder();
|
||||
for (int idx = 0; idx < rowValues.size(); idx++) {
|
||||
rowBuilder.withValue(columns.get(idx), rowValues.get(idx).toJson());
|
||||
}
|
||||
|
||||
builder.withRow(rowBuilder.build());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
// end region
|
||||
|
||||
// region helper functions
|
||||
|
||||
private long timeToLong(Time time) {
|
||||
|
||||
+12
-4
@@ -29,6 +29,7 @@ import co.elastic.clients.elasticsearch.core.search.Suggestion;
|
||||
import co.elastic.clients.elasticsearch.core.search.TotalHits;
|
||||
import co.elastic.clients.json.JsonpMapper;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -56,6 +57,7 @@ import org.springframework.util.CollectionUtils;
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.4
|
||||
*/
|
||||
class SearchDocumentResponseBuilder {
|
||||
@@ -83,8 +85,10 @@ class SearchDocumentResponseBuilder {
|
||||
Map<String, List<Suggestion<EntityAsMap>>> suggest = responseBody.suggest();
|
||||
var pointInTimeId = responseBody.pitId();
|
||||
var shards = responseBody.shards();
|
||||
var executionDurationInMillis = responseBody.took();
|
||||
|
||||
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
|
||||
return from(hitsMetadata, shards, scrollId, pointInTimeId, executionDurationInMillis, aggregations, suggest,
|
||||
entityCreator, jsonpMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,8 +113,10 @@ class SearchDocumentResponseBuilder {
|
||||
var aggregations = response.aggregations();
|
||||
var suggest = response.suggest();
|
||||
var pointInTimeId = response.pitId();
|
||||
var executionDurationInMillis = response.took();
|
||||
|
||||
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
|
||||
return from(hitsMetadata, shards, scrollId, pointInTimeId, executionDurationInMillis, aggregations, suggest,
|
||||
entityCreator, jsonpMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +133,7 @@ class SearchDocumentResponseBuilder {
|
||||
* @return the {@link SearchDocumentResponse}
|
||||
*/
|
||||
public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable ShardStatistics shards,
|
||||
@Nullable String scrollId, @Nullable String pointInTimeId, @Nullable Map<String, Aggregate> aggregations,
|
||||
@Nullable String scrollId, @Nullable String pointInTimeId, long executionDurationInMillis, @Nullable Map<String, Aggregate> aggregations,
|
||||
Map<String, List<Suggestion<EntityAsMap>>> suggestES, SearchDocumentResponse.EntityCreator<T> entityCreator,
|
||||
JsonpMapper jsonpMapper) {
|
||||
|
||||
@@ -151,6 +157,8 @@ class SearchDocumentResponseBuilder {
|
||||
|
||||
float maxScore = hitsMetadata.maxScore() != null ? hitsMetadata.maxScore().floatValue() : Float.NaN;
|
||||
|
||||
Duration executionDuration = Duration.ofMillis(executionDurationInMillis);
|
||||
|
||||
List<SearchDocument> searchDocuments = new ArrayList<>();
|
||||
for (Hit<?> hit : hitsMetadata.hits()) {
|
||||
searchDocuments.add(DocumentAdapters.from(hit, jsonpMapper));
|
||||
@@ -163,7 +171,7 @@ class SearchDocumentResponseBuilder {
|
||||
|
||||
SearchShardStatistics shardStatistics = shards != null ? shardsFrom(shards) : null;
|
||||
|
||||
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchDocuments,
|
||||
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, pointInTimeId, searchDocuments,
|
||||
aggregationsContainer, suggest, shardStatistics);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverte
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
|
||||
import org.springframework.data.elasticsearch.core.script.ScriptOperations;
|
||||
import org.springframework.data.elasticsearch.core.sql.SqlOperations;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -35,7 +36,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Dmitriy Yakovlev
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
public interface ElasticsearchOperations extends DocumentOperations, SearchOperations, ScriptOperations {
|
||||
public interface ElasticsearchOperations extends DocumentOperations, SearchOperations, ScriptOperations, SqlOperations {
|
||||
|
||||
/**
|
||||
* get an {@link IndexOperations} that is bound to the given class
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
|
||||
import org.springframework.data.elasticsearch.core.script.ReactiveScriptOperations;
|
||||
import org.springframework.data.elasticsearch.core.sql.ReactiveSqlOperations;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -31,7 +32,7 @@ import org.springframework.lang.Nullable;
|
||||
* @since 3.2
|
||||
*/
|
||||
public interface ReactiveElasticsearchOperations
|
||||
extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations {
|
||||
extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations, ReactiveSqlOperations {
|
||||
|
||||
/**
|
||||
* Get the {@link ElasticsearchConverter} used.
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -25,6 +27,7 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @param <T> the result data class.
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.4
|
||||
*/
|
||||
public interface ReactiveSearchHits<T> {
|
||||
@@ -37,6 +40,11 @@ public interface ReactiveSearchHits<T> {
|
||||
|
||||
float getMaxScore();
|
||||
|
||||
/**
|
||||
* @return the execution duration it took to complete the request
|
||||
*/
|
||||
Duration getExecutionDuration();
|
||||
|
||||
/**
|
||||
* @return the {@link SearchHit}s from the search result.
|
||||
*/
|
||||
|
||||
@@ -17,11 +17,14 @@ package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.4
|
||||
*/
|
||||
public class ReactiveSearchHitsImpl<T> implements ReactiveSearchHits<T> {
|
||||
@@ -58,6 +61,11 @@ public class ReactiveSearchHitsImpl<T> implements ReactiveSearchHits<T> {
|
||||
return delegate.getMaxScore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getExecutionDuration() {
|
||||
return delegate.getExecutionDuration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSearchHits() {
|
||||
return delegate.hasSearchHits();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
@@ -47,6 +48,7 @@ import org.springframework.util.Assert;
|
||||
* @author Sascha Woo
|
||||
* @author Jakob Hoeper
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SearchHitMapping<T> {
|
||||
@@ -87,6 +89,7 @@ public class SearchHitMapping<T> {
|
||||
long totalHits = searchDocumentResponse.getTotalHits();
|
||||
SearchShardStatistics shardStatistics = searchDocumentResponse.getSearchShardStatistics();
|
||||
float maxScore = searchDocumentResponse.getMaxScore();
|
||||
Duration executionDuration = searchDocumentResponse.getExecutionDuration();
|
||||
String scrollId = searchDocumentResponse.getScrollId();
|
||||
String pointInTimeId = searchDocumentResponse.getPointInTimeId();
|
||||
|
||||
@@ -104,8 +107,8 @@ public class SearchHitMapping<T> {
|
||||
Suggest suggest = searchDocumentResponse.getSuggest();
|
||||
mapHitsInCompletionSuggestion(suggest);
|
||||
|
||||
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchHits,
|
||||
aggregations, suggest, shardStatistics);
|
||||
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, pointInTimeId,
|
||||
searchHits, aggregations, suggest, shardStatistics);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -238,6 +241,7 @@ public class SearchHitMapping<T> {
|
||||
return new SearchHitsImpl<>(searchHits.getTotalHits(),
|
||||
searchHits.getTotalHitsRelation(),
|
||||
searchHits.getMaxScore(),
|
||||
searchHits.getExecutionDuration(),
|
||||
scrollId,
|
||||
searchHits.getPointInTimeId(),
|
||||
convertedSearchHits,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@@ -28,6 +29,7 @@ import org.springframework.lang.Nullable;
|
||||
* @param <T> the result data class.
|
||||
* @author Sascha Woo
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface SearchHits<T> extends Streamable<SearchHit<T>> {
|
||||
@@ -43,6 +45,11 @@ public interface SearchHits<T> extends Streamable<SearchHit<T>> {
|
||||
*/
|
||||
float getMaxScore();
|
||||
|
||||
/**
|
||||
* @return the execution duration it took to complete the request
|
||||
*/
|
||||
Duration getExecutionDuration();
|
||||
|
||||
/**
|
||||
* @param index position in List.
|
||||
* @return the {@link SearchHit} at position {index}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -30,6 +31,7 @@ import org.springframework.util.Assert;
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Sascha Woo
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SearchHitsImpl<T> implements SearchScrollHits<T> {
|
||||
@@ -37,6 +39,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
|
||||
private final long totalHits;
|
||||
private final TotalHitsRelation totalHitsRelation;
|
||||
private final float maxScore;
|
||||
private final Duration executionDuration;
|
||||
@Nullable private final String scrollId;
|
||||
private final List<? extends SearchHit<T>> searchHits;
|
||||
private final Lazy<List<SearchHit<T>>> unmodifiableSearchHits;
|
||||
@@ -49,12 +52,13 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
|
||||
* @param totalHits the number of total hits for the search
|
||||
* @param totalHitsRelation the relation {@see TotalHitsRelation}, must not be {@literal null}
|
||||
* @param maxScore the maximum score
|
||||
* @param executionDuration the execution duration it took to complete the request
|
||||
* @param scrollId the scroll id if available
|
||||
* @param searchHits must not be {@literal null}
|
||||
* @param aggregations the aggregations if available
|
||||
*/
|
||||
public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, @Nullable String scrollId,
|
||||
@Nullable String pointInTimeId, List<? extends SearchHit<T>> searchHits,
|
||||
public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, Duration executionDuration,
|
||||
@Nullable String scrollId, @Nullable String pointInTimeId, List<? extends SearchHit<T>> searchHits,
|
||||
@Nullable AggregationsContainer<?> aggregations, @Nullable Suggest suggest,
|
||||
@Nullable SearchShardStatistics searchShardStatistics) {
|
||||
|
||||
@@ -63,6 +67,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
|
||||
this.totalHits = totalHits;
|
||||
this.totalHitsRelation = totalHitsRelation;
|
||||
this.maxScore = maxScore;
|
||||
this.executionDuration = executionDuration;
|
||||
this.scrollId = scrollId;
|
||||
this.pointInTimeId = pointInTimeId;
|
||||
this.searchHits = searchHits;
|
||||
@@ -88,6 +93,11 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
|
||||
return maxScore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getExecutionDuration() {
|
||||
return executionDuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getScrollId() {
|
||||
@@ -133,6 +143,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
|
||||
"totalHits=" + totalHits + //
|
||||
", totalHitsRelation=" + totalHitsRelation + //
|
||||
", maxScore=" + maxScore + //
|
||||
", executionDuration=" + executionDuration + //
|
||||
", scrollId='" + scrollId + '\'' + //
|
||||
", pointInTimeId='" + pointInTimeId + '\'' + //
|
||||
", searchHits={" + searchHits.size() + " elements}" + //
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.data.util.CloseableIterator;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -23,6 +25,7 @@ import org.springframework.lang.Nullable;
|
||||
* {@link java.util.stream.Stream}.
|
||||
*
|
||||
* @author Sascha Woo
|
||||
* @author Mohamed El Harrougui
|
||||
* @param <T>
|
||||
* @since 4.0
|
||||
*/
|
||||
@@ -39,6 +42,11 @@ public interface SearchHitsIterator<T> extends CloseableIterator<SearchHit<T>> {
|
||||
*/
|
||||
float getMaxScore();
|
||||
|
||||
/**
|
||||
* @return the execution duration it took to complete the request
|
||||
*/
|
||||
Duration getExecutionDuration();
|
||||
|
||||
/**
|
||||
* @return the number of total hits.
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
@@ -31,6 +32,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Sascha Woo
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 3.2
|
||||
*/
|
||||
abstract class StreamQueries {
|
||||
@@ -56,6 +58,7 @@ abstract class StreamQueries {
|
||||
|
||||
AggregationsContainer<?> aggregations = searchHits.getAggregations();
|
||||
float maxScore = searchHits.getMaxScore();
|
||||
Duration executionDuration = searchHits.getExecutionDuration();
|
||||
long totalHits = searchHits.getTotalHits();
|
||||
TotalHitsRelation totalHitsRelation = searchHits.getTotalHitsRelation();
|
||||
|
||||
@@ -86,6 +89,11 @@ abstract class StreamQueries {
|
||||
return maxScore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getExecutionDuration() {
|
||||
return executionDuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalHits() {
|
||||
return totalHits;
|
||||
|
||||
+10
-2
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core.document;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
@@ -29,6 +30,7 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SearchDocumentResponse {
|
||||
@@ -36,6 +38,7 @@ public class SearchDocumentResponse {
|
||||
private final long totalHits;
|
||||
private final String totalHitsRelation;
|
||||
private final float maxScore;
|
||||
private final Duration executionDuration;
|
||||
@Nullable private final String scrollId;
|
||||
private final List<SearchDocument> searchDocuments;
|
||||
@Nullable private final AggregationsContainer<?> aggregations;
|
||||
@@ -44,13 +47,14 @@ public class SearchDocumentResponse {
|
||||
@Nullable String pointInTimeId;
|
||||
@Nullable private final SearchShardStatistics searchShardStatistics;
|
||||
|
||||
public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, @Nullable String scrollId,
|
||||
@Nullable String pointInTimeId, List<SearchDocument> searchDocuments,
|
||||
public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, Duration executionDuration,
|
||||
@Nullable String scrollId, @Nullable String pointInTimeId, List<SearchDocument> searchDocuments,
|
||||
@Nullable AggregationsContainer<?> aggregationsContainer, @Nullable Suggest suggest,
|
||||
@Nullable SearchShardStatistics searchShardStatistics) {
|
||||
this.totalHits = totalHits;
|
||||
this.totalHitsRelation = totalHitsRelation;
|
||||
this.maxScore = maxScore;
|
||||
this.executionDuration = executionDuration;
|
||||
this.scrollId = scrollId;
|
||||
this.pointInTimeId = pointInTimeId;
|
||||
this.searchDocuments = searchDocuments;
|
||||
@@ -71,6 +75,10 @@ public class SearchDocumentResponse {
|
||||
return maxScore;
|
||||
}
|
||||
|
||||
public Duration getExecutionDuration() {
|
||||
return executionDuration;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getScrollId() {
|
||||
return scrollId;
|
||||
|
||||
+23
-9
@@ -69,6 +69,7 @@ import com.fasterxml.jackson.databind.util.RawValue;
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Xiao Yu
|
||||
* @author Subhobrata Dey
|
||||
* @author Andriy Redko
|
||||
*/
|
||||
public class MappingBuilder {
|
||||
|
||||
@@ -175,7 +176,9 @@ public class MappingBuilder {
|
||||
.findAnnotation(org.springframework.data.elasticsearch.annotations.Document.class);
|
||||
var dynamicMapping = docAnnotation != null ? docAnnotation.dynamic() : null;
|
||||
|
||||
mapEntity(objectNode, entity, true, "", false, FieldType.Auto, null, dynamicMapping, runtimeFields);
|
||||
final FieldType fieldType = FieldType.Auto;
|
||||
mapEntity(objectNode, entity, true, "", false, fieldType, fieldType.getMappedName(), null, dynamicMapping,
|
||||
runtimeFields);
|
||||
|
||||
if (!excludeFromSource.isEmpty()) {
|
||||
ObjectNode sourceNode = objectNode.putObject(SOURCE);
|
||||
@@ -211,6 +214,7 @@ public class MappingBuilder {
|
||||
|
||||
private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentEntity<?> entity,
|
||||
boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType,
|
||||
String fieldTypeMappedName,
|
||||
@Nullable Field parentFieldAnnotation, @Nullable Dynamic dynamicMapping, @Nullable Document runtimeFields)
|
||||
throws IOException {
|
||||
|
||||
@@ -244,7 +248,7 @@ public class MappingBuilder {
|
||||
boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField);
|
||||
if (writeNestedProperties) {
|
||||
|
||||
String type = nestedOrObjectField ? fieldType.getMappedName() : FieldType.Object.getMappedName();
|
||||
String type = nestedOrObjectField ? fieldTypeMappedName : FieldType.Object.getMappedName();
|
||||
|
||||
ObjectNode nestedObjectNode = objectMapper.createObjectNode();
|
||||
nestedObjectNode.put(FIELD_PARAM_TYPE, type);
|
||||
@@ -345,8 +349,10 @@ public class MappingBuilder {
|
||||
: nestedPropertyPrefix + '.' + property.getFieldName();
|
||||
|
||||
Field fieldAnnotation = property.findAnnotation(Field.class);
|
||||
MultiField multiFieldAnnotation = property.findAnnotation(MultiField.class);
|
||||
|
||||
if (fieldAnnotation != null && fieldAnnotation.excludeFromSource()) {
|
||||
if ((fieldAnnotation != null && fieldAnnotation.excludeFromSource()) ||
|
||||
multiFieldAnnotation != null && multiFieldAnnotation.mainField().excludeFromSource()) {
|
||||
excludeFromSource.add(nestedPropertyPath);
|
||||
}
|
||||
|
||||
@@ -370,15 +376,13 @@ public class MappingBuilder {
|
||||
nestedPropertyPrefix = nestedPropertyPath;
|
||||
|
||||
mapEntity(propertiesNode, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(),
|
||||
fieldAnnotation, dynamicMapping, null);
|
||||
getMappedTypeName(fieldAnnotation), fieldAnnotation, dynamicMapping, null);
|
||||
|
||||
nestedPropertyPrefix = currentNestedPropertyPrefix;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MultiField multiField = property.findAnnotation(MultiField.class);
|
||||
|
||||
if (isCompletionProperty) {
|
||||
CompletionField completionField = property.findAnnotation(CompletionField.class);
|
||||
applyCompletionFieldMapping(propertiesNode, property, completionField);
|
||||
@@ -386,8 +390,8 @@ public class MappingBuilder {
|
||||
|
||||
if (isRootObject && fieldAnnotation != null && property.isIdProperty()) {
|
||||
applyDefaultIdFieldMapping(propertiesNode, property);
|
||||
} else if (multiField != null) {
|
||||
addMultiFieldMapping(propertiesNode, property, multiField, isNestedOrObjectProperty, dynamicMapping);
|
||||
} else if (multiFieldAnnotation != null) {
|
||||
addMultiFieldMapping(propertiesNode, property, multiFieldAnnotation, isNestedOrObjectProperty, dynamicMapping);
|
||||
} else if (fieldAnnotation != null) {
|
||||
addSingleFieldMapping(propertiesNode, property, fieldAnnotation, isNestedOrObjectProperty, dynamicMapping);
|
||||
}
|
||||
@@ -473,7 +477,7 @@ public class MappingBuilder {
|
||||
}
|
||||
|
||||
propertiesNode.set(property.getFieldName(), objectMapper.createObjectNode() //
|
||||
.put(FIELD_PARAM_TYPE, field.type().getMappedName()) //
|
||||
.put(FIELD_PARAM_TYPE, getMappedTypeName(field)) //
|
||||
.put(MAPPING_ENABLED, false) //
|
||||
);
|
||||
|
||||
@@ -482,6 +486,16 @@ public class MappingBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mapping type name to be used for the {@link Field}
|
||||
*
|
||||
* @param field field to return the mapping type name for
|
||||
* @return the mapping type name
|
||||
*/
|
||||
private String getMappedTypeName(Field field) {
|
||||
return StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : field.type().getMappedName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add mapping for @Field annotation
|
||||
*
|
||||
|
||||
+65
-14
@@ -23,15 +23,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.elasticsearch.annotations.DateFormat;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.IndexOptions;
|
||||
import org.springframework.data.elasticsearch.annotations.IndexPrefixes;
|
||||
import org.springframework.data.elasticsearch.annotations.InnerField;
|
||||
import org.springframework.data.elasticsearch.annotations.NullValueType;
|
||||
import org.springframework.data.elasticsearch.annotations.Similarity;
|
||||
import org.springframework.data.elasticsearch.annotations.TermVector;
|
||||
import org.springframework.data.elasticsearch.annotations.*;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -49,6 +41,7 @@ import com.fasterxml.jackson.databind.node.TextNode;
|
||||
* @author Brian Kimmig
|
||||
* @author Morgan Lutz
|
||||
* @author Sascha Woo
|
||||
* @author Haibo Liu
|
||||
* @since 4.0
|
||||
*/
|
||||
public final class MappingParameters {
|
||||
@@ -78,6 +71,10 @@ public final class MappingParameters {
|
||||
static final String FIELD_PARAM_ORIENTATION = "orientation";
|
||||
static final String FIELD_PARAM_POSITIVE_SCORE_IMPACT = "positive_score_impact";
|
||||
static final String FIELD_PARAM_DIMS = "dims";
|
||||
static final String FIELD_PARAM_ELEMENT_TYPE = "element_type";
|
||||
static final String FIELD_PARAM_M = "m";
|
||||
static final String FIELD_PARAM_EF_CONSTRUCTION = "ef_construction";
|
||||
static final String FIELD_PARAM_CONFIDENCE_INTERVAL = "confidence_interval";
|
||||
static final String FIELD_PARAM_SCALING_FACTOR = "scaling_factor";
|
||||
static final String FIELD_PARAM_SEARCH_ANALYZER = "search_analyzer";
|
||||
static final String FIELD_PARAM_STORE = "store";
|
||||
@@ -110,12 +107,16 @@ public final class MappingParameters {
|
||||
private final Integer positionIncrementGap;
|
||||
private final boolean positiveScoreImpact;
|
||||
private final Integer dims;
|
||||
private final String elementType;
|
||||
private final KnnSimilarity knnSimilarity;
|
||||
@Nullable private final KnnIndexOptions knnIndexOptions;
|
||||
private final String searchAnalyzer;
|
||||
private final double scalingFactor;
|
||||
private final String similarity;
|
||||
private final boolean store;
|
||||
private final TermVector termVector;
|
||||
private final FieldType type;
|
||||
private final String mappedTypeName;
|
||||
|
||||
/**
|
||||
* extracts the mapping parameters from the relevant annotations.
|
||||
@@ -141,6 +142,7 @@ public final class MappingParameters {
|
||||
store = field.store();
|
||||
fielddata = field.fielddata();
|
||||
type = field.type();
|
||||
mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName();
|
||||
dateFormats = field.format();
|
||||
dateFormatPatterns = field.pattern();
|
||||
analyzer = field.analyzer();
|
||||
@@ -171,9 +173,12 @@ public final class MappingParameters {
|
||||
positiveScoreImpact = field.positiveScoreImpact();
|
||||
dims = field.dims();
|
||||
if (type == FieldType.Dense_Vector) {
|
||||
Assert.isTrue(dims >= 1 && dims <= 2048,
|
||||
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 2048.");
|
||||
Assert.isTrue(dims >= 1 && dims <= 4096,
|
||||
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 4096.");
|
||||
}
|
||||
elementType = field.elementType();
|
||||
knnSimilarity = field.knnSimilarity();
|
||||
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
|
||||
Assert.isTrue(field.enabled() || type == FieldType.Object, "enabled false is only allowed for field type object");
|
||||
enabled = field.enabled();
|
||||
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
|
||||
@@ -184,6 +189,7 @@ public final class MappingParameters {
|
||||
store = field.store();
|
||||
fielddata = field.fielddata();
|
||||
type = field.type();
|
||||
mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName();
|
||||
dateFormats = field.format();
|
||||
dateFormatPatterns = field.pattern();
|
||||
analyzer = field.analyzer();
|
||||
@@ -214,9 +220,12 @@ public final class MappingParameters {
|
||||
positiveScoreImpact = field.positiveScoreImpact();
|
||||
dims = field.dims();
|
||||
if (type == FieldType.Dense_Vector) {
|
||||
Assert.isTrue(dims >= 1 && dims <= 2048,
|
||||
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 2048.");
|
||||
Assert.isTrue(dims >= 1 && dims <= 4096,
|
||||
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 4096.");
|
||||
}
|
||||
elementType = field.elementType();
|
||||
knnSimilarity = field.knnSimilarity();
|
||||
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
|
||||
enabled = true;
|
||||
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
|
||||
}
|
||||
@@ -239,7 +248,7 @@ public final class MappingParameters {
|
||||
}
|
||||
|
||||
if (type != FieldType.Auto) {
|
||||
objectNode.put(FIELD_PARAM_TYPE, type.getMappedName());
|
||||
objectNode.put(FIELD_PARAM_TYPE, mappedTypeName);
|
||||
|
||||
if (type == FieldType.Date || type == FieldType.Date_Nanos || type == FieldType.Date_Range) {
|
||||
List<String> formats = new ArrayList<>();
|
||||
@@ -356,6 +365,48 @@ public final class MappingParameters {
|
||||
|
||||
if (type == FieldType.Dense_Vector) {
|
||||
objectNode.put(FIELD_PARAM_DIMS, dims);
|
||||
|
||||
if (!FieldElementType.DEFAULT.equals(elementType)) {
|
||||
objectNode.put(FIELD_PARAM_ELEMENT_TYPE, elementType);
|
||||
}
|
||||
|
||||
if (knnSimilarity != KnnSimilarity.DEFAULT) {
|
||||
objectNode.put(FIELD_PARAM_SIMILARITY, knnSimilarity.getSimilarity());
|
||||
}
|
||||
|
||||
if (knnSimilarity != KnnSimilarity.DEFAULT) {
|
||||
Assert.isTrue(index, "knn similarity can only be specified when 'index' is true.");
|
||||
objectNode.put(FIELD_PARAM_SIMILARITY, knnSimilarity.getSimilarity());
|
||||
}
|
||||
|
||||
if (knnIndexOptions != null) {
|
||||
Assert.isTrue(index, "knn index options can only be specified when 'index' is true.");
|
||||
ObjectNode indexOptionsNode = objectNode.putObject(FIELD_PARAM_INDEX_OPTIONS);
|
||||
KnnAlgorithmType algoType = knnIndexOptions.type();
|
||||
if (algoType != KnnAlgorithmType.DEFAULT) {
|
||||
if (algoType == KnnAlgorithmType.INT8_HNSW || algoType == KnnAlgorithmType.INT8_FLAT) {
|
||||
Assert.isTrue(!FieldElementType.BYTE.equals(elementType),
|
||||
"'element_type' can only be float when using vector quantization.");
|
||||
}
|
||||
indexOptionsNode.put(FIELD_PARAM_TYPE, algoType.getType());
|
||||
}
|
||||
if (knnIndexOptions.m() >= 0) {
|
||||
Assert.isTrue(algoType == KnnAlgorithmType.HNSW || algoType == KnnAlgorithmType.INT8_HNSW,
|
||||
"knn 'm' parameter can only be applicable to hnsw and int8_hnsw index types.");
|
||||
indexOptionsNode.put(FIELD_PARAM_M, knnIndexOptions.m());
|
||||
}
|
||||
if (knnIndexOptions.efConstruction() >= 0) {
|
||||
Assert.isTrue(algoType == KnnAlgorithmType.HNSW || algoType == KnnAlgorithmType.INT8_HNSW,
|
||||
"knn 'ef_construction' can only be applicable to hnsw and int8_hnsw index types.");
|
||||
indexOptionsNode.put(FIELD_PARAM_EF_CONSTRUCTION, knnIndexOptions.efConstruction());
|
||||
}
|
||||
if (knnIndexOptions.confidenceInterval() >= 0) {
|
||||
Assert.isTrue(algoType == KnnAlgorithmType.INT8_HNSW
|
||||
|| algoType == KnnAlgorithmType.INT8_FLAT,
|
||||
"knn 'confidence_interval' can only be applicable to int8_hnsw and int8_flat index types.");
|
||||
indexOptionsNode.put(FIELD_PARAM_CONFIDENCE_INTERVAL, knnIndexOptions.confidenceInterval());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2024 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 java.util.Objects;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.query.Query;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Immutable Value object encapsulating index alias(es).
|
||||
*
|
||||
* @author Youssef Aouichaoui
|
||||
* @since 5.4
|
||||
*/
|
||||
public class Alias {
|
||||
/**
|
||||
* Alias name for the index.
|
||||
*/
|
||||
private final String alias;
|
||||
|
||||
/**
|
||||
* Query used to limit documents the alias can access.
|
||||
*/
|
||||
@Nullable private final Query filter;
|
||||
|
||||
/**
|
||||
* Used to route indexing operations to a specific shard.
|
||||
*/
|
||||
@Nullable private final String indexRouting;
|
||||
|
||||
/**
|
||||
* Used to route search operations to a specific shard.
|
||||
*/
|
||||
@Nullable private final String searchRouting;
|
||||
|
||||
/**
|
||||
* Used to route indexing and search operations to a specific shard.
|
||||
*/
|
||||
@Nullable private final String routing;
|
||||
|
||||
/**
|
||||
* The alias is hidden? By default, this is set to {@code false}.
|
||||
*/
|
||||
@Nullable private final Boolean isHidden;
|
||||
|
||||
/**
|
||||
* The index is the 'write index' for the alias? By default, this is set to {@code false}.
|
||||
*/
|
||||
@Nullable private final Boolean isWriteIndex;
|
||||
|
||||
private Alias(Builder builder) {
|
||||
this.alias = builder.alias;
|
||||
|
||||
this.filter = builder.filter;
|
||||
|
||||
this.indexRouting = builder.indexRouting;
|
||||
this.searchRouting = builder.searchRouting;
|
||||
this.routing = builder.routing;
|
||||
|
||||
this.isHidden = builder.isHidden;
|
||||
this.isWriteIndex = builder.isWriteIndex;
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Query getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getIndexRouting() {
|
||||
return indexRouting;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSearchRouting() {
|
||||
return searchRouting;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getRouting() {
|
||||
return routing;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getHidden() {
|
||||
return isHidden;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getWriteIndex() {
|
||||
return isWriteIndex;
|
||||
}
|
||||
|
||||
public static Builder builder(String alias) {
|
||||
return new Builder(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof Alias that))
|
||||
return false;
|
||||
|
||||
return Objects.equals(alias, that.alias) && Objects.equals(filter, that.filter)
|
||||
&& Objects.equals(indexRouting, that.indexRouting)
|
||||
&& Objects.equals(searchRouting, that.searchRouting)
|
||||
&& Objects.equals(routing, that.routing)
|
||||
&& Objects.equals(isHidden, that.isHidden)
|
||||
&& Objects.equals(isWriteIndex, that.isWriteIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(alias, filter, indexRouting, searchRouting, routing, isHidden, isWriteIndex);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final String alias;
|
||||
|
||||
@Nullable private Query filter;
|
||||
|
||||
@Nullable private String indexRouting;
|
||||
@Nullable private String searchRouting;
|
||||
@Nullable private String routing;
|
||||
|
||||
@Nullable private Boolean isHidden;
|
||||
@Nullable private Boolean isWriteIndex;
|
||||
|
||||
public Builder(String alias) {
|
||||
Assert.notNull(alias, "alias must not be null");
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query used to limit documents the alias can access.
|
||||
*/
|
||||
public Builder withFilter(@Nullable Query filter) {
|
||||
this.filter = filter;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to route indexing operations to a specific shard.
|
||||
*/
|
||||
public Builder withIndexRouting(@Nullable String indexRouting) {
|
||||
if (indexRouting != null && !indexRouting.trim().isEmpty()) {
|
||||
this.indexRouting = indexRouting;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to route search operations to a specific shard.
|
||||
*/
|
||||
public Builder withSearchRouting(@Nullable String searchRouting) {
|
||||
if (searchRouting != null && !searchRouting.trim().isEmpty()) {
|
||||
this.searchRouting = searchRouting;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to route indexing and search operations to a specific shard.
|
||||
*/
|
||||
public Builder withRouting(@Nullable String routing) {
|
||||
if (routing != null && !routing.trim().isEmpty()) {
|
||||
this.routing = routing;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The alias is hidden? By default, this is set to {@code false}.
|
||||
*/
|
||||
public Builder withHidden(@Nullable Boolean hidden) {
|
||||
isHidden = hidden;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The index is the 'write index' for the alias? By default, this is set to {@code false}.
|
||||
*/
|
||||
public Builder withWriteIndex(@Nullable Boolean writeIndex) {
|
||||
isWriteIndex = writeIndex;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Alias build() {
|
||||
return new Alias(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2024 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 java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.document.Document;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Encapsulating index mapping fields, settings, and index alias(es).
|
||||
*
|
||||
* @author Youssef Aouichaoui
|
||||
* @since 5.3
|
||||
*/
|
||||
public class CreateIndexSettings {
|
||||
private final IndexCoordinates indexCoordinates;
|
||||
private final Set<Alias> aliases;
|
||||
|
||||
@Nullable private final Map<String, Object> settings;
|
||||
|
||||
@Nullable private final Document mapping;
|
||||
|
||||
private CreateIndexSettings(Builder builder) {
|
||||
this.indexCoordinates = builder.indexCoordinates;
|
||||
this.aliases = builder.aliases;
|
||||
|
||||
this.settings = builder.settings;
|
||||
this.mapping = builder.mapping;
|
||||
}
|
||||
|
||||
public static Builder builder(IndexCoordinates indexCoordinates) {
|
||||
return new Builder(indexCoordinates);
|
||||
}
|
||||
|
||||
public IndexCoordinates getIndexCoordinates() {
|
||||
return indexCoordinates;
|
||||
}
|
||||
|
||||
public Alias[] getAliases() {
|
||||
return aliases.toArray(Alias[]::new);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Map<String, Object> getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Document getMapping() {
|
||||
return mapping;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final IndexCoordinates indexCoordinates;
|
||||
private final Set<Alias> aliases = new HashSet<>();
|
||||
|
||||
@Nullable private Map<String, Object> settings;
|
||||
|
||||
@Nullable private Document mapping;
|
||||
|
||||
public Builder(IndexCoordinates indexCoordinates) {
|
||||
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
|
||||
this.indexCoordinates = indexCoordinates;
|
||||
}
|
||||
|
||||
public Builder withAlias(Alias alias) {
|
||||
Assert.notNull(alias, "alias must not be null");
|
||||
this.aliases.add(alias);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAliases(Set<Alias> aliases) {
|
||||
Assert.notNull(aliases, "aliases must not be null");
|
||||
this.aliases.addAll(aliases);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSettings(Map<String, Object> settings) {
|
||||
Assert.notNull(settings, "settings must not be null");
|
||||
this.settings = settings;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMapping(@Nullable Document mapping) {
|
||||
this.mapping = mapping;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public CreateIndexSettings build() {
|
||||
return new CreateIndexSettings(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core.mapping;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Dynamic;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
@@ -42,6 +44,14 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
|
||||
|
||||
IndexCoordinates getIndexCoordinates();
|
||||
|
||||
/**
|
||||
* Retrieves the aliases associated with the current entity.
|
||||
*
|
||||
* @return Returns a set of aliases of the {@link PersistentEntity}.
|
||||
* @since 5.4
|
||||
*/
|
||||
Set<Alias> getAliases();
|
||||
|
||||
short getShards();
|
||||
|
||||
short getReplicas();
|
||||
@@ -66,7 +76,7 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
|
||||
boolean isCreateIndexAndMapping();
|
||||
|
||||
/**
|
||||
* returns the {@link ElasticsearchPersistentProperty} with the given fieldName (may be set by the {@link Field}
|
||||
* returns the {@link ElasticsearchPersistentProperty} with the given fieldName (can be set by the {@link Field})
|
||||
* annotation.
|
||||
*
|
||||
* @param fieldName to field name for the search, must not be {@literal null}
|
||||
@@ -189,7 +199,7 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
|
||||
boolean storeVersionInSource();
|
||||
|
||||
/**
|
||||
* @return if the mapping should be written to the index on repositry bootstrap even if the index already exists.
|
||||
* @return if the mapping should be written to the index on repository bootstrap even if the index already exists.
|
||||
* @since 5.2
|
||||
*/
|
||||
boolean isAlwaysWriteMapping();
|
||||
|
||||
+1
-1
@@ -34,12 +34,12 @@ public class IndexCoordinates {
|
||||
private final String[] indexNames;
|
||||
|
||||
public static IndexCoordinates of(String... indexNames) {
|
||||
Assert.notNull(indexNames, "indexNames must not be null");
|
||||
return new IndexCoordinates(indexNames);
|
||||
}
|
||||
|
||||
private IndexCoordinates(String... indexNames) {
|
||||
Assert.notEmpty(indexNames, "indexNames may not be null or empty");
|
||||
Assert.noNullElements(indexNames, "indexNames may not contain null elements");
|
||||
this.indexNames = indexNames;
|
||||
}
|
||||
|
||||
|
||||
+44
-2
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core.mapping;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -31,6 +33,8 @@ import org.springframework.data.elasticsearch.annotations.Routing;
|
||||
import org.springframework.data.elasticsearch.annotations.Setting;
|
||||
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.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||
@@ -80,6 +84,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
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);
|
||||
@@ -112,6 +117,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
this.dynamic = document.dynamic();
|
||||
this.storeIdInSource = document.storeIdInSource();
|
||||
this.storeVersionInSource = document.storeVersionInSource();
|
||||
buildAliases();
|
||||
} else {
|
||||
this.dynamic = Dynamic.INHERIT;
|
||||
this.storeIdInSource = true;
|
||||
@@ -138,6 +144,11 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return resolve(IndexCoordinates.of(getIndexName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Alias> getAliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getIndexStoreType() {
|
||||
@@ -247,12 +258,12 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
if (property.isIndexedIndexNameProperty()) {
|
||||
|
||||
if (!property.getActualType().isAssignableFrom(String.class)) {
|
||||
throw new MappingException(String.format("@IndexedIndexName annotation must be put on String property"));
|
||||
throw new MappingException("@IndexedIndexName annotation must be put on String property");
|
||||
}
|
||||
|
||||
if (indexedIndexNameProperty != null) {
|
||||
throw new MappingException(
|
||||
String.format("@IndexedIndexName annotation can only be put on one property in an entity"));
|
||||
"@IndexedIndexName annotation can only be put on one property in an entity");
|
||||
}
|
||||
|
||||
this.indexedIndexNameProperty = property;
|
||||
@@ -615,4 +626,35 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
public Dynamic dynamic() {
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Building once the aliases for the current document.
|
||||
*/
|
||||
private void buildAliases() {
|
||||
// Clear the existing aliases.
|
||||
aliases.clear();
|
||||
|
||||
if (document != null) {
|
||||
for (org.springframework.data.elasticsearch.annotations.Alias alias : document.aliases()) {
|
||||
if (alias.value().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Query query = null;
|
||||
if (!alias.filter().value().isEmpty()) {
|
||||
query = new StringQuery(alias.filter().value());
|
||||
}
|
||||
|
||||
aliases.add(
|
||||
Alias.builder(alias.value())
|
||||
.withFilter(query)
|
||||
.withIndexRouting(alias.indexRouting())
|
||||
.withSearchRouting(alias.searchRouting())
|
||||
.withRouting(alias.routing())
|
||||
.withHidden(alias.isHidden())
|
||||
.withWriteIndex(alias.isWriteIndex())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -98,6 +98,7 @@ public class SimpleElasticsearchPersistentProperty extends
|
||||
this.isSeqNoPrimaryTerm = SeqNoPrimaryTerm.class.isAssignableFrom(getRawType());
|
||||
|
||||
boolean isField = isAnnotationPresent(Field.class);
|
||||
boolean isMultiField = isAnnotationPresent(MultiField.class);
|
||||
|
||||
if (isVersionProperty() && !getType().equals(Long.class)) {
|
||||
throw new MappingException(String.format("Version property %s must be of type Long!", property.getName()));
|
||||
@@ -109,8 +110,10 @@ public class SimpleElasticsearchPersistentProperty extends
|
||||
|
||||
initPropertyValueConverter();
|
||||
|
||||
storeNullValue = isField && getRequiredAnnotation(Field.class).storeNullValue();
|
||||
storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue() : true;
|
||||
storeNullValue = isField ? getRequiredAnnotation(Field.class).storeNullValue()
|
||||
: isMultiField && getRequiredAnnotation(MultiField.class).mainField().storeNullValue();
|
||||
storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue()
|
||||
: !isMultiField || getRequiredAnnotation(MultiField.class).mainField().storeEmptyValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* Copyright 2024 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.query;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Defines an SQL request.
|
||||
*
|
||||
* @author Aouichaoui Youssef
|
||||
* @see <a href= "https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">docs</a>
|
||||
* @since 5.4
|
||||
*/
|
||||
public class SqlQuery {
|
||||
|
||||
/**
|
||||
* If true, returns partial results if there are shard request timeouts or shard failures.
|
||||
* <p>
|
||||
* Default, this is set to {@code false}.
|
||||
*/
|
||||
@Nullable private final Boolean allowPartialSearchResults;
|
||||
|
||||
/**
|
||||
* Default catalog/cluster for queries. If unspecified, the queries are executed on the data in the local cluster
|
||||
* only.
|
||||
*/
|
||||
@Nullable private final String catalog;
|
||||
|
||||
/**
|
||||
* If true, returns results in a columnar format.
|
||||
* <p>
|
||||
* Default, this is set to {@code false}.
|
||||
*/
|
||||
@Nullable private final Boolean columnar;
|
||||
|
||||
/**
|
||||
* To retrieve a set of paginated results, ignore other request body parameters when specifying a cursor and using the
|
||||
* {@link #columnar} and {@link #timeZone} parameters.
|
||||
*/
|
||||
@Nullable private final String cursor;
|
||||
|
||||
/**
|
||||
* Maximum number of rows to return in the response.
|
||||
* <p>
|
||||
* Default, this is set to {@code 1000}.
|
||||
*/
|
||||
@Nullable private final Integer fetchSize;
|
||||
|
||||
/**
|
||||
* If false, the API returns an error for fields containing array values.
|
||||
* <p>
|
||||
* Default, this is set to {@code false}.
|
||||
*/
|
||||
@Nullable private final Boolean fieldMultiValueLeniency;
|
||||
|
||||
/**
|
||||
* Query that filter documents for the SQL search.
|
||||
*/
|
||||
@Nullable private final Query filter;
|
||||
|
||||
/**
|
||||
* If true, the search can run on frozen indices.
|
||||
* <p>
|
||||
* Default, this is set to {@code false}.
|
||||
*/
|
||||
@Nullable private final Boolean indexIncludeFrozen;
|
||||
|
||||
/**
|
||||
* Retention period for an async or saved synchronous search.
|
||||
* <p>
|
||||
* Default, this is set to {@code 5 days}.
|
||||
*/
|
||||
@Nullable private final Duration keepAlive;
|
||||
|
||||
/**
|
||||
* If it is true, it will store synchronous searches when the {@link #waitForCompletionTimeout} parameter is
|
||||
* specified.
|
||||
*/
|
||||
@Nullable private final Boolean keepOnCompletion;
|
||||
|
||||
/**
|
||||
* Minimum retention period for the scroll cursor.
|
||||
* <p>
|
||||
* Default, this is set to {@code 45 seconds}.
|
||||
*/
|
||||
@Nullable private final Duration pageTimeout;
|
||||
|
||||
/**
|
||||
* Timeout before the request fails.
|
||||
* <p>
|
||||
* Default, this is set to {@code 90 seconds}.
|
||||
*/
|
||||
@Nullable private final Duration requestTimeout;
|
||||
|
||||
/**
|
||||
* Values for parameters in the query.
|
||||
*/
|
||||
@Nullable private final List<Object> params;
|
||||
|
||||
/**
|
||||
* SQL query to run.
|
||||
*/
|
||||
private final String query;
|
||||
|
||||
/**
|
||||
* Time zone ID for the search.
|
||||
* <p>
|
||||
* Default, this is set to {@code UTC}.
|
||||
*/
|
||||
@Nullable private final TimeZone timeZone;
|
||||
|
||||
/**
|
||||
* Period to wait for complete results.
|
||||
* <p>
|
||||
* Default, this is set to no timeout.
|
||||
*/
|
||||
@Nullable private final Duration waitForCompletionTimeout;
|
||||
|
||||
private SqlQuery(Builder builder) {
|
||||
this.allowPartialSearchResults = builder.allowPartialSearchResults;
|
||||
|
||||
this.catalog = builder.catalog;
|
||||
this.columnar = builder.columnar;
|
||||
this.cursor = builder.cursor;
|
||||
|
||||
this.fetchSize = builder.fetchSize;
|
||||
this.fieldMultiValueLeniency = builder.fieldMultiValueLeniency;
|
||||
|
||||
this.filter = builder.filter;
|
||||
|
||||
this.indexIncludeFrozen = builder.indexIncludeFrozen;
|
||||
this.keepAlive = builder.keepAlive;
|
||||
this.keepOnCompletion = builder.keepOnCompletion;
|
||||
|
||||
this.pageTimeout = builder.pageTimeout;
|
||||
this.requestTimeout = builder.requestTimeout;
|
||||
|
||||
this.params = builder.params;
|
||||
this.query = builder.query;
|
||||
|
||||
this.timeZone = builder.timeZone;
|
||||
this.waitForCompletionTimeout = builder.waitForCompletionTimeout;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getAllowPartialSearchResults() {
|
||||
return allowPartialSearchResults;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getCatalog() {
|
||||
return catalog;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getColumnar() {
|
||||
return columnar;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getFetchSize() {
|
||||
return fetchSize;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getFieldMultiValueLeniency() {
|
||||
return fieldMultiValueLeniency;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Query getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getIndexIncludeFrozen() {
|
||||
return indexIncludeFrozen;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Duration getKeepAlive() {
|
||||
return keepAlive;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getKeepOnCompletion() {
|
||||
return keepOnCompletion;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Duration getPageTimeout() {
|
||||
return pageTimeout;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Duration getRequestTimeout() {
|
||||
return requestTimeout;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Object> getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TimeZone getTimeZone() {
|
||||
return timeZone;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Duration getWaitForCompletionTimeout() {
|
||||
return waitForCompletionTimeout;
|
||||
}
|
||||
|
||||
public static Builder builder(String query) {
|
||||
return new Builder(query);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
@Nullable private Boolean allowPartialSearchResults;
|
||||
|
||||
@Nullable private String catalog;
|
||||
@Nullable private Boolean columnar;
|
||||
@Nullable private String cursor;
|
||||
|
||||
@Nullable private Integer fetchSize;
|
||||
@Nullable private Boolean fieldMultiValueLeniency;
|
||||
|
||||
@Nullable private Query filter;
|
||||
|
||||
@Nullable private Boolean indexIncludeFrozen;
|
||||
|
||||
@Nullable private Duration keepAlive;
|
||||
@Nullable private Boolean keepOnCompletion;
|
||||
|
||||
@Nullable private Duration pageTimeout;
|
||||
@Nullable private Duration requestTimeout;
|
||||
|
||||
@Nullable private List<Object> params;
|
||||
private final String query;
|
||||
|
||||
@Nullable private TimeZone timeZone;
|
||||
@Nullable private Duration waitForCompletionTimeout;
|
||||
|
||||
private Builder(String query) {
|
||||
Assert.notNull(query, "query must not be null");
|
||||
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, returns partial results if there are shard request timeouts or shard failures.
|
||||
*/
|
||||
public Builder withAllowPartialSearchResults(Boolean allowPartialSearchResults) {
|
||||
this.allowPartialSearchResults = allowPartialSearchResults;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default catalog/cluster for queries. If unspecified, the queries are executed on the data in the local cluster
|
||||
* only.
|
||||
*/
|
||||
public Builder withCatalog(String catalog) {
|
||||
this.catalog = catalog;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, returns results in a columnar format.
|
||||
*/
|
||||
public Builder withColumnar(Boolean columnar) {
|
||||
this.columnar = columnar;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* To retrieve a set of paginated results, ignore other request body parameters when specifying a cursor and using
|
||||
* the {@link #columnar} and {@link #timeZone} parameters.
|
||||
*/
|
||||
public Builder withCursor(String cursor) {
|
||||
this.cursor = cursor;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of rows to return in the response.
|
||||
*/
|
||||
public Builder withFetchSize(Integer fetchSize) {
|
||||
this.fetchSize = fetchSize;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If false, the API returns an error for fields containing array values.
|
||||
*/
|
||||
public Builder withFieldMultiValueLeniency(Boolean fieldMultiValueLeniency) {
|
||||
this.fieldMultiValueLeniency = fieldMultiValueLeniency;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query that filter documents for the SQL search.
|
||||
*/
|
||||
public Builder setFilter(Query filter) {
|
||||
this.filter = filter;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, the search can run on frozen indices.
|
||||
*/
|
||||
public Builder withIndexIncludeFrozen(Boolean indexIncludeFrozen) {
|
||||
this.indexIncludeFrozen = indexIncludeFrozen;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retention period for an async or saved synchronous search.
|
||||
*/
|
||||
public Builder setKeepAlive(Duration keepAlive) {
|
||||
this.keepAlive = keepAlive;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If it is true, it will store synchronous searches when the {@link #waitForCompletionTimeout} parameter is
|
||||
* specified.
|
||||
*/
|
||||
public Builder withKeepOnCompletion(Boolean keepOnCompletion) {
|
||||
this.keepOnCompletion = keepOnCompletion;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum retention period for the scroll cursor.
|
||||
*/
|
||||
public Builder withPageTimeout(Duration pageTimeout) {
|
||||
this.pageTimeout = pageTimeout;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout before the request fails.
|
||||
*/
|
||||
public Builder withRequestTimeout(Duration requestTimeout) {
|
||||
this.requestTimeout = requestTimeout;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Values for parameters in the query.
|
||||
*/
|
||||
public Builder withParams(List<Object> params) {
|
||||
this.params = params;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value for parameters in the query.
|
||||
*/
|
||||
public Builder withParam(Object param) {
|
||||
if (this.params == null) {
|
||||
this.params = new ArrayList<>();
|
||||
}
|
||||
this.params.add(param);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time zone ID for the search.
|
||||
*/
|
||||
public Builder withTimeZone(TimeZone timeZone) {
|
||||
this.timeZone = timeZone;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Period to wait for complete results.
|
||||
*/
|
||||
public Builder withWaitForCompletionTimeout(Duration waitForCompletionTimeout) {
|
||||
this.waitForCompletionTimeout = waitForCompletionTimeout;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SqlQuery build() {
|
||||
return new SqlQuery(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2024 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.sql;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.query.SqlQuery;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* The reactive version of operations for the
|
||||
* <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">SQL search API</a>.
|
||||
*
|
||||
* @author Aouichaoui Youssef
|
||||
* @since 5.4
|
||||
*/
|
||||
public interface ReactiveSqlOperations {
|
||||
/**
|
||||
* Execute the sql {@code query} against elasticsearch and return result as {@link SqlResponse}
|
||||
*
|
||||
* @param query the query to execute
|
||||
* @return {@link SqlResponse} containing the list of found objects
|
||||
*/
|
||||
Mono<SqlResponse> search(SqlQuery query);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2024 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.sql;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.query.SqlQuery;
|
||||
|
||||
/**
|
||||
* The operations for the
|
||||
* <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">SQL search API</a>.
|
||||
*
|
||||
* @author Aouichaoui Youssef
|
||||
* @since 5.4
|
||||
*/
|
||||
public interface SqlOperations {
|
||||
/**
|
||||
* Execute the sql {@code query} against elasticsearch and return result as {@link SqlResponse}
|
||||
*
|
||||
* @param query the query to execute
|
||||
* @return {@link SqlResponse} containing the list of found objects
|
||||
*/
|
||||
SqlResponse search(SqlQuery query);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2024 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.sql;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
|
||||
import jakarta.json.JsonValue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Defines an SQL response.
|
||||
*
|
||||
* @author Aouichaoui Youssef
|
||||
* @see <a href= "https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">docs</a>
|
||||
* @since 5.4
|
||||
*/
|
||||
public class SqlResponse {
|
||||
/**
|
||||
* If {@code true}, the search is still running.
|
||||
*/
|
||||
private final boolean running;
|
||||
|
||||
/**
|
||||
* If {@code true}, the response does not contain complete search results.
|
||||
*/
|
||||
private final boolean partial;
|
||||
|
||||
/**
|
||||
* Cursor for the next set of paginated results.
|
||||
*/
|
||||
@Nullable private final String cursor;
|
||||
|
||||
/**
|
||||
* Column headings for the search results.
|
||||
*/
|
||||
private final List<Column> columns;
|
||||
|
||||
/**
|
||||
* Values for the search results.
|
||||
*/
|
||||
private final List<Row> rows;
|
||||
|
||||
private SqlResponse(Builder builder) {
|
||||
this.running = builder.running;
|
||||
this.partial = builder.partial;
|
||||
|
||||
this.cursor = builder.cursor;
|
||||
|
||||
this.columns = unmodifiableList(builder.columns);
|
||||
this.rows = unmodifiableList(builder.rows);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public boolean isPartial() {
|
||||
return partial;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public List<Column> getColumns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public List<Row> getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public record Column(String name, String type) {
|
||||
}
|
||||
|
||||
public static class Row implements Iterable<Map.Entry<Column, JsonValue>> {
|
||||
private final Map<Column, JsonValue> row;
|
||||
|
||||
private Row(Builder builder) {
|
||||
this.row = builder.row;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<Map.Entry<Column, JsonValue>> iterator() {
|
||||
return row.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public JsonValue get(Column column) {
|
||||
return row.get(column);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final Map<Column, JsonValue> row = new HashMap<>();
|
||||
|
||||
public Builder withValue(Column column, JsonValue value) {
|
||||
this.row.put(column, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Row build() {
|
||||
return new Row(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private boolean running;
|
||||
private boolean partial;
|
||||
|
||||
@Nullable private String cursor;
|
||||
|
||||
private final List<Column> columns = new ArrayList<>();
|
||||
private final List<Row> rows = new ArrayList<>();
|
||||
|
||||
private Builder() {}
|
||||
|
||||
/**
|
||||
* If {@code true}, the search is still running.
|
||||
*/
|
||||
public Builder withRunning(boolean running) {
|
||||
this.running = running;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@code true}, the response does not contain complete search results.
|
||||
*/
|
||||
public Builder withPartial(boolean partial) {
|
||||
this.partial = partial;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor for the next set of paginated results.
|
||||
*/
|
||||
public Builder withCursor(@Nullable String cursor) {
|
||||
this.cursor = cursor;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column headings for the search results.
|
||||
*/
|
||||
public Builder withColumns(List<Column> columns) {
|
||||
this.columns.addAll(columns);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column heading for the search results.
|
||||
*/
|
||||
public Builder withColumn(Column column) {
|
||||
this.columns.add(column);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Values for the search results.
|
||||
*/
|
||||
public Builder withRows(List<Row> rows) {
|
||||
this.rows.addAll(rows);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value for the search results.
|
||||
*/
|
||||
public Builder withRow(Row row) {
|
||||
this.rows.add(row);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SqlResponse build() {
|
||||
return new SqlResponse(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Classes and interfaces to access to SQL API of Elasticsearch.
|
||||
*/
|
||||
@org.springframework.lang.NonNullApi
|
||||
@org.springframework.lang.NonNullFields
|
||||
package org.springframework.data.elasticsearch.core.sql;
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.utils.geohash;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -84,7 +85,7 @@ public class Geohash {
|
||||
Assert.notNull(geohash, "geohash must not be null");
|
||||
|
||||
var point = Geohash.toPoint(geohash);
|
||||
return String.format("%f,%f", point.getLat(), point.getLon());
|
||||
return String.format(Locale.ROOT, "%f,%f", point.getLat(), point.getLon());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Spring Data Elasticsearch 5.3 GA (2024.0.0)
|
||||
Spring Data Elasticsearch 5.4.1 (2024.1.1)
|
||||
Copyright (c) [2013-2022] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
@@ -22,3 +22,7 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+76
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch;
|
||||
|
||||
import static co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders.*;
|
||||
import static java.util.UUID.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
|
||||
|
||||
@@ -28,6 +30,7 @@ import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -37,6 +40,7 @@ import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.InnerField;
|
||||
import org.springframework.data.elasticsearch.annotations.MultiField;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.IndexOperations;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
@@ -373,6 +377,42 @@ public abstract class NestedObjectIntegrationTests {
|
||||
assertThat(books.getSearchHit(0).getContent().getId()).isEqualTo(book2.getId());
|
||||
}
|
||||
|
||||
@Test // #2952
|
||||
@DisplayName("should handle null and empty field parameters in the mapping process")
|
||||
void shouldSupportMappingNullAndEmptyFieldParameter() {
|
||||
// Given
|
||||
operations.indexOps(MultiFieldWithNullEmptyParameters.class).createWithMapping();
|
||||
List<IndexQuery> indexQueries = new ArrayList<>();
|
||||
MultiFieldWithNullEmptyParameters nullObj = new MultiFieldWithNullEmptyParameters();
|
||||
nullObj.addFieldWithInner(randomUUID().toString());
|
||||
MultiFieldWithNullEmptyParameters objWithValue = new MultiFieldWithNullEmptyParameters();
|
||||
objWithValue.addEmptyField(randomUUID().toString());
|
||||
|
||||
IndexQuery indexQuery1 = new IndexQuery();
|
||||
indexQuery1.setId(nextIdAsString());
|
||||
indexQuery1.setObject(nullObj);
|
||||
indexQueries.add(indexQuery1);
|
||||
|
||||
IndexQuery indexQuery2 = new IndexQuery();
|
||||
indexQuery2.setId(nextIdAsString());
|
||||
indexQuery2.setObject(objWithValue);
|
||||
indexQueries.add(indexQuery2);
|
||||
|
||||
// When
|
||||
operations.bulkIndex(indexQueries, MultiFieldWithNullEmptyParameters.class);
|
||||
|
||||
// Then
|
||||
SearchHits<MultiFieldWithNullEmptyParameters> nullResults = operations.search(
|
||||
NativeQuery.builder().withQuery(match(bm -> bm.field("empty-field").query("EMPTY"))).build(),
|
||||
MultiFieldWithNullEmptyParameters.class);
|
||||
assertThat(nullResults.getSearchHits()).hasSize(1);
|
||||
|
||||
nullResults = operations.search(
|
||||
NativeQuery.builder().withQuery(match(bm -> bm.field("inner-field.prefix").query("EMPTY"))).build(),
|
||||
MultiFieldWithNullEmptyParameters.class);
|
||||
assertThat(nullResults.getSearchHits()).hasSize(1);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
abstract protected Query getNestedQuery4();
|
||||
|
||||
@@ -622,4 +662,40 @@ public abstract class NestedObjectIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}-multi-field")
|
||||
static class MultiFieldWithNullEmptyParameters {
|
||||
@Nullable
|
||||
@MultiField(mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY",
|
||||
storeNullValue = true)) private List<String> emptyField;
|
||||
|
||||
@Nullable
|
||||
@MultiField(mainField = @Field(name = "inner-field", type = FieldType.Text, storeNullValue = true),
|
||||
otherFields = { @InnerField(suffix = "prefix", type = FieldType.Keyword,
|
||||
nullValue = "EMPTY") }) private List<String> fieldWithInner;
|
||||
|
||||
public List<String> getEmptyField() {
|
||||
if (emptyField == null) {
|
||||
emptyField = new ArrayList<>();
|
||||
}
|
||||
|
||||
return emptyField;
|
||||
}
|
||||
|
||||
public void addEmptyField(String value) {
|
||||
getEmptyField().add(value);
|
||||
}
|
||||
|
||||
public List<String> getFieldWithInner() {
|
||||
if (fieldWithInner == null) {
|
||||
fieldWithInner = new ArrayList<>();
|
||||
}
|
||||
|
||||
return fieldWithInner;
|
||||
}
|
||||
|
||||
public void addFieldWithInner(@Nullable String value) {
|
||||
getFieldWithInner().add(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2024 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.blockhound;
|
||||
|
||||
import reactor.blockhound.BlockHound;
|
||||
import reactor.blockhound.BlockingOperationError;
|
||||
import reactor.blockhound.integration.BlockHoundIntegration;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
public class BlockHoundIntegrationCustomizer implements BlockHoundIntegration {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(BlockHoundIntegrationCustomizer.class);
|
||||
|
||||
@Override
|
||||
public void applyTo(BlockHound.Builder builder) {
|
||||
// Elasticsearch classes reading from the classpath on initialization, needed for parsing Elasticsearch responses
|
||||
builder //
|
||||
.allowBlockingCallsInside("org.elasticsearch.Build", "<clinit>") //
|
||||
.allowBlockingCallsInside("org.elasticsearch.common.xcontent.XContentBuilder", "<clinit>") // pre 7.16
|
||||
.allowBlockingCallsInside("org.elasticsearch.common.XContentBuilder", "<clinit>") // from 7.16 on
|
||||
.allowBlockingCallsInside("org.elasticsearch.xcontent.json.JsonXContent", "contentBuilder") // from 7.16 on
|
||||
.allowBlockingCallsInside("jakarta.json.spi.JsonProvider", "provider") //
|
||||
;
|
||||
builder.blockingMethodCallback(it -> {
|
||||
LOGGER.error("BlockHound error", new Error(it.toString()));
|
||||
throw new BlockingOperationError(it);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2024 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.blockhound;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import reactor.blockhound.BlockingOperationError;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
public class BlockHoundTests {
|
||||
|
||||
@Test // #1822
|
||||
@DisplayName("should fail if BlockHound is not installed")
|
||||
void shouldFailIfBlockHoundIsNotInstalled() {
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
Mono.delay(Duration.ofMillis(1)).doOnNext(it -> {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}).block(); // should throw an exception about Thread.sleep
|
||||
}).hasCauseInstanceOf(BlockingOperationError.class);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import co.elastic.clients.elasticsearch._types.FieldValue;
|
||||
import co.elastic.clients.elasticsearch._types.mapping.TypeMapping;
|
||||
import co.elastic.clients.elasticsearch.cluster.HealthRequest;
|
||||
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
|
||||
import co.elastic.clients.elasticsearch.core.CountRequest;
|
||||
import co.elastic.clients.elasticsearch.core.CountResponse;
|
||||
import co.elastic.clients.elasticsearch.core.IndexRequest;
|
||||
import co.elastic.clients.elasticsearch.core.IndexResponse;
|
||||
import co.elastic.clients.elasticsearch.core.SearchRequest;
|
||||
@@ -62,6 +64,7 @@ import org.springframework.lang.Nullable;
|
||||
* on port 9200 and an intercepting proxy on port 8080.
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
* @author maryantocinn
|
||||
*/
|
||||
@Disabled
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@@ -352,6 +355,43 @@ public class DevTests {
|
||||
private ResponseBody<EntityAsMap> searchReactive(SearchRequest searchRequest) {
|
||||
return Objects.requireNonNull(reactiveElasticsearchClient.search(searchRequest, EntityAsMap.class).block());
|
||||
}
|
||||
|
||||
// endregion
|
||||
// region count
|
||||
@Test
|
||||
@Order(40)
|
||||
void count() {
|
||||
|
||||
CountRequest countRequest = new CountRequest.Builder().index(INDEX)
|
||||
.query(query -> query.match(matchQuery -> matchQuery.field("content").query(FieldValue.of("content1"))))
|
||||
.build();
|
||||
|
||||
CountResponse countResponse = null;
|
||||
|
||||
try {
|
||||
countResponse = countImperative(countRequest);
|
||||
assertThat(countResponse).isNotNull();
|
||||
assertThat(countResponse.count()).isEqualTo(1);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("error", e);
|
||||
}
|
||||
|
||||
try {
|
||||
countResponse = countReactive(countRequest);
|
||||
assertThat(countResponse).isNotNull();
|
||||
assertThat(countResponse.count()).isEqualTo(1);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private CountResponse countImperative(CountRequest countRequest) throws IOException {
|
||||
return imperativeElasticsearchClient.count(countRequest);
|
||||
}
|
||||
|
||||
private CountResponse countReactive(CountRequest countRequest) {
|
||||
return Objects.requireNonNull(reactiveElasticsearchClient.count(countRequest).block());
|
||||
}
|
||||
// endregion
|
||||
|
||||
private ClientConfiguration clientConfiguration() {
|
||||
|
||||
+17
@@ -30,12 +30,16 @@ import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
|
||||
import org.springframework.data.elasticsearch.core.query.Criteria;
|
||||
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.DocValueField;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Han Seungwoo
|
||||
*/
|
||||
class RequestConverterTest {
|
||||
|
||||
@@ -72,6 +76,19 @@ class RequestConverterTest {
|
||||
assertThat(fieldAndFormats.get(1).format()).isEqualTo("format2");
|
||||
}
|
||||
|
||||
@Test // #2973
|
||||
@DisplayName("should set refresh based on deleteRequest")
|
||||
void refreshSetByDeleteRequest() {
|
||||
var query = new CriteriaQuery(new Criteria("text").contains("test"));
|
||||
var deleteQuery = DeleteQuery.builder(query).withRefresh(true).build();
|
||||
|
||||
var deleteByQueryRequest = requestConverter.documentDeleteByQueryRequest(deleteQuery, null, SampleEntity.class,
|
||||
IndexCoordinates.of("foo"),
|
||||
null);
|
||||
|
||||
assertThat(deleteByQueryRequest.refresh()).isTrue();
|
||||
}
|
||||
|
||||
@Document(indexName = "does-not-matter")
|
||||
static class SampleEntity {
|
||||
@Nullable
|
||||
|
||||
+21
-52
@@ -44,6 +44,7 @@ import com.google.common.collect.ImmutableMap;
|
||||
*
|
||||
* @author Sébastien Comeau
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
* @since 5.2
|
||||
*/
|
||||
class SearchDocumentResponseBuilderUnitTests {
|
||||
@@ -54,35 +55,21 @@ class SearchDocumentResponseBuilderUnitTests {
|
||||
void shouldGetPhraseSuggestion() throws JSONException {
|
||||
// arrange
|
||||
final var hitsMetadata = new HitsMetadata.Builder<EntityAsMap>()
|
||||
.total(total -> total
|
||||
.value(0)
|
||||
.relation(TotalHitsRelation.Eq))
|
||||
.hits(new ArrayList<>())
|
||||
.build();
|
||||
.total(total -> total.value(0).relation(TotalHitsRelation.Eq)).hits(new ArrayList<>()).build();
|
||||
|
||||
final var suggestionTest = new Suggestion.Builder<EntityAsMap>()
|
||||
.phrase(phrase -> phrase
|
||||
.text("National")
|
||||
.offset(0)
|
||||
.length(8)
|
||||
.options(option -> option
|
||||
.text("nations")
|
||||
.highlighted("highlighted-nations")
|
||||
.score(0.11480146)
|
||||
.collateMatch(false))
|
||||
.options(option -> option
|
||||
.text("national")
|
||||
.highlighted("highlighted-national")
|
||||
.score(0.08063514)
|
||||
.collateMatch(false)))
|
||||
final var suggestionTest = new Suggestion.Builder<EntityAsMap>().phrase(phrase -> phrase.text("National").offset(0)
|
||||
.length(8)
|
||||
.options(
|
||||
option -> option.text("nations").highlighted("highlighted-nations").score(0.11480146).collateMatch(false))
|
||||
.options(option -> option.text("national").highlighted("highlighted-national").score(0.08063514)
|
||||
.collateMatch(false)))
|
||||
.build();
|
||||
|
||||
final var sortProperties = ImmutableMap.<String, List<Suggestion<EntityAsMap>>> builder()
|
||||
.put("suggestionTest", ImmutableList.of(suggestionTest))
|
||||
.build();
|
||||
.put("suggestionTest", ImmutableList.of(suggestionTest)).build();
|
||||
|
||||
// act
|
||||
final var actual = SearchDocumentResponseBuilder.from(hitsMetadata, null, null, null, null, sortProperties, null,
|
||||
final var actual = SearchDocumentResponseBuilder.from(hitsMetadata, null, null, null, 0, null, sortProperties, null,
|
||||
jsonpMapper);
|
||||
|
||||
// assert
|
||||
@@ -122,35 +109,19 @@ class SearchDocumentResponseBuilderUnitTests {
|
||||
void shouldGetShardStatisticsInfo() {
|
||||
// arrange
|
||||
HitsMetadata<EntityAsMap> hitsMetadata = new HitsMetadata.Builder<EntityAsMap>()
|
||||
.total(t -> t
|
||||
.value(0)
|
||||
.relation(TotalHitsRelation.Eq))
|
||||
.hits(new ArrayList<>())
|
||||
.build();
|
||||
.total(t -> t.value(0).relation(TotalHitsRelation.Eq)).hits(new ArrayList<>()).build();
|
||||
|
||||
ShardStatistics shards = new ShardStatistics.Builder()
|
||||
.total(15)
|
||||
.successful(14)
|
||||
.skipped(0)
|
||||
.failed(1)
|
||||
.failures(List.of(
|
||||
ShardFailure.of(sfb -> sfb
|
||||
.index("test-index")
|
||||
.node("test-node")
|
||||
.shard(1)
|
||||
.reason(rb -> rb
|
||||
.reason("this is a mock failure in shards")
|
||||
.causedBy(cbb -> cbb.reason("inner reason")
|
||||
.metadata(Map.of("hello", JsonData.of("world"))))
|
||||
.type("reason-type")
|
||||
ShardStatistics shards = new ShardStatistics.Builder().total(15).successful(14).skipped(0).failed(1)
|
||||
.failures(List.of(ShardFailure.of(sfb -> sfb.index("test-index").node("test-node").shard(1)
|
||||
.reason(rb -> rb.reason("this is a mock failure in shards")
|
||||
.causedBy(cbb -> cbb.reason("inner reason").metadata(Map.of("hello", JsonData.of("world"))))
|
||||
.type("reason-type")
|
||||
|
||||
)
|
||||
.status("fail"))))
|
||||
.build();
|
||||
).status("fail")))).build();
|
||||
|
||||
// act
|
||||
SearchDocumentResponse response = SearchDocumentResponseBuilder.from(hitsMetadata, shards, null, null,
|
||||
null, null, null, jsonpMapper);
|
||||
SearchDocumentResponse response = SearchDocumentResponseBuilder.from(hitsMetadata, shards, null, null, 0, null,
|
||||
null, null, jsonpMapper);
|
||||
|
||||
// assert
|
||||
SearchShardStatistics shardStatistics = response.getSearchShardStatistics();
|
||||
@@ -164,11 +135,9 @@ class SearchDocumentResponseBuilderUnitTests {
|
||||
assertThat(failures.size()).isEqualTo(1);
|
||||
assertThat(failures).extracting(SearchShardStatistics.Failure::getIndex).containsExactly("test-index");
|
||||
assertThat(failures).extracting(SearchShardStatistics.Failure::getElasticsearchErrorCause)
|
||||
.extracting(ElasticsearchErrorCause::getReason)
|
||||
.containsExactly("this is a mock failure in shards");
|
||||
.extracting(ElasticsearchErrorCause::getReason).containsExactly("this is a mock failure in shards");
|
||||
assertThat(failures).extracting(SearchShardStatistics.Failure::getElasticsearchErrorCause)
|
||||
.extracting(ElasticsearchErrorCause::getCausedBy)
|
||||
.extracting(ElasticsearchErrorCause::getReason)
|
||||
.extracting(ElasticsearchErrorCause::getCausedBy).extracting(ElasticsearchErrorCause::getReason)
|
||||
.containsExactly("inner reason");
|
||||
}
|
||||
}
|
||||
|
||||
+35
-37
@@ -190,44 +190,42 @@ public class ElasticsearchELCIntegrationTests extends ElasticsearchIntegrationTe
|
||||
@Override
|
||||
protected Query getQueryWithRescorer() {
|
||||
|
||||
return NativeQuery.builder() //
|
||||
.withQuery(q -> q //
|
||||
.bool(b -> b //
|
||||
.filter(f -> f.exists(e -> e.field("rate"))) //
|
||||
.should(s -> s.term(t -> t.field("message").value("message"))) //
|
||||
)) //
|
||||
.withRescorerQuery( //
|
||||
new RescorerQuery(NativeQuery.builder() //
|
||||
.withQuery(q -> q //
|
||||
.functionScore(fs -> fs //
|
||||
.functions(f1 -> f1 //
|
||||
.filter(matchAllQueryAsQuery()) //
|
||||
.weight(1.0) //
|
||||
.gauss(d -> d //
|
||||
.field("rate") //
|
||||
.placement(dp -> dp //
|
||||
.origin(JsonData.of(0)) //
|
||||
.scale(JsonData.of(10)) //
|
||||
.decay(0.5)) //
|
||||
)) //
|
||||
.functions(f2 -> f2 //
|
||||
.filter(matchAllQueryAsQuery()).weight(100.0) //
|
||||
.gauss(d -> d //
|
||||
.field("rate") //
|
||||
.placement(dp -> dp //
|
||||
.origin(JsonData.of(0)) //
|
||||
.scale(JsonData.of(10)) //
|
||||
.decay(0.5)) //
|
||||
return NativeQuery.builder()
|
||||
.withQuery(q -> q
|
||||
.bool(b -> b
|
||||
.filter(f -> f.exists(e -> e.field("rate")))
|
||||
.should(s -> s.term(t -> t.field("message").value("message")))))
|
||||
.withRescorerQuery(
|
||||
new RescorerQuery(NativeQuery.builder()
|
||||
.withQuery(q -> q
|
||||
.functionScore(fs -> fs
|
||||
.functions(f1 -> f1
|
||||
.filter(matchAllQueryAsQuery())
|
||||
.weight(1.0)
|
||||
.gauss(d -> d
|
||||
.untyped(ut -> ut
|
||||
.field("rate")
|
||||
.placement(dp -> dp
|
||||
.origin(JsonData.of(0))
|
||||
.scale(JsonData.of(10))
|
||||
.decay(0.5)))))
|
||||
.functions(f2 -> f2
|
||||
.filter(matchAllQueryAsQuery()).weight(100.0)
|
||||
.gauss(d -> d
|
||||
.untyped(ut -> ut
|
||||
.field("rate")
|
||||
.placement(dp -> dp
|
||||
.origin(JsonData.of(0))
|
||||
.scale(JsonData.of(10))
|
||||
.decay(0.5)))
|
||||
|
||||
)) //
|
||||
.scoreMode(FunctionScoreMode.Sum) //
|
||||
.maxBoost(80.0) //
|
||||
.boostMode(FunctionBoostMode.Replace)) //
|
||||
) //
|
||||
.build() //
|
||||
) //
|
||||
.withScoreMode(RescorerQuery.ScoreMode.Max) //
|
||||
.withWindowSize(100)) //
|
||||
))
|
||||
.scoreMode(FunctionScoreMode.Sum)
|
||||
.maxBoost(80.0)
|
||||
.boostMode(FunctionBoostMode.Replace)))
|
||||
.build())
|
||||
.withScoreMode(RescorerQuery.ScoreMode.Max)
|
||||
.withWindowSize(100))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -24,6 +24,7 @@ import static org.springframework.data.elasticsearch.core.query.StringQuery.*;
|
||||
import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
|
||||
import static org.springframework.data.elasticsearch.utils.IndexBuilder.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -99,6 +100,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author scoobyzhang
|
||||
* @author Hamid Rahimi
|
||||
* @author Illia Ulianov
|
||||
* @author Mohamed El Harrougui
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
public abstract class ElasticsearchIntegrationTests {
|
||||
@@ -1855,7 +1857,7 @@ public abstract class ElasticsearchIntegrationTests {
|
||||
protected abstract Query getBoolQueryWithWildcardsFirstMustSecondShouldAndMinScore(String firstField,
|
||||
String firstValue, String secondField, String secondValue, float minScore);
|
||||
|
||||
@Test // DATAES-462
|
||||
@Test // DATAES-462, #2986
|
||||
public void shouldReturnScores() {
|
||||
|
||||
List<IndexQuery> indexQueries = new ArrayList<>();
|
||||
@@ -1872,6 +1874,7 @@ public abstract class ElasticsearchIntegrationTests {
|
||||
IndexCoordinates.of(indexNameProvider.indexName()));
|
||||
|
||||
assertThat(searchHits.getMaxScore()).isGreaterThan(0f);
|
||||
assertThat(searchHits.getExecutionDuration().toMillis()).isGreaterThan(0);
|
||||
assertThat(searchHits.getSearchHit(0).getScore()).isGreaterThan(0f);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -23,11 +23,12 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
class IndexCoordinatesUnitTests {
|
||||
|
||||
@Test
|
||||
void cannotBeInitializedWithNullIndexName() {
|
||||
assertThatThrownBy(() -> IndexCoordinates.of(null)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> IndexCoordinates.of((String) null)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+8
-7
@@ -54,12 +54,13 @@ public class LogEntityELCIntegrationTests extends LogEntityIntegrationTests {
|
||||
|
||||
@Override
|
||||
Query rangeQueryForIp(String from, String to) {
|
||||
return NativeQuery.builder() //
|
||||
.withQuery(qb -> qb //
|
||||
.range(rqb -> rqb //
|
||||
.field("ip") //
|
||||
.gte(JsonData.of(from))//
|
||||
.lte(JsonData.of(to))//
|
||||
)).build();
|
||||
return NativeQuery.builder()
|
||||
.withQuery(qb -> qb
|
||||
.range(rqb -> rqb
|
||||
.untyped(ut -> ut
|
||||
.field("ip")
|
||||
.gte(JsonData.of(from))
|
||||
.lte(JsonData.of(to)))))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import static java.util.Collections.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
@@ -33,6 +34,7 @@ import org.springframework.data.util.CloseableIterator;
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
*/
|
||||
class SearchHitSupportTest {
|
||||
|
||||
@@ -65,8 +67,8 @@ class SearchHitSupportTest {
|
||||
hits.add(new SearchHit<>(null, null, null, 0, null, null, null, null, null, null, "four"));
|
||||
hits.add(new SearchHit<>(null, null, null, 0, null, null, null, null, null, null, "five"));
|
||||
|
||||
SearchHits<String> originalSearchHits = new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, "scroll",
|
||||
null, hits, null, null, null);
|
||||
SearchHits<String> originalSearchHits = new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0,
|
||||
Duration.ofMillis(1), "scroll", null, hits, null, null, null);
|
||||
|
||||
SearchPage<String> searchPage = SearchHitSupport.searchPageFor(originalSearchHits, PageRequest.of(0, 3));
|
||||
SearchHits<String> searchHits = searchPage.getSearchHits();
|
||||
@@ -89,6 +91,11 @@ class SearchHitSupportTest {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getExecutionDuration() {
|
||||
return Duration.ofMillis(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalHits() {
|
||||
return 2;
|
||||
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -32,6 +33,7 @@ import org.springframework.data.util.StreamUtils;
|
||||
* @author Sascha Woo
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @author Mohamed El Harrougui
|
||||
*/
|
||||
public class StreamQueriesTest {
|
||||
|
||||
@@ -181,6 +183,7 @@ public class StreamQueriesTest {
|
||||
}
|
||||
|
||||
private SearchScrollHits<String> newSearchScrollHits(List<SearchHit<String>> hits, String scrollId) {
|
||||
return new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, scrollId, null, hits, null, null, null);
|
||||
return new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, Duration.ofMillis(1), scrollId, null, hits,
|
||||
null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
+62
-2
@@ -15,12 +15,15 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core.index;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -30,13 +33,18 @@ import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Alias;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.Filter;
|
||||
import org.springframework.data.elasticsearch.annotations.Mapping;
|
||||
import org.springframework.data.elasticsearch.annotations.Setting;
|
||||
import org.springframework.data.elasticsearch.client.elc.Queries;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.IndexInformation;
|
||||
import org.springframework.data.elasticsearch.core.IndexOperations;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -63,7 +71,7 @@ public abstract class IndexOperationsIntegrationTests {
|
||||
@Test
|
||||
@Order(java.lang.Integer.MAX_VALUE)
|
||||
void cleanup() {
|
||||
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete();
|
||||
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete();
|
||||
}
|
||||
|
||||
@Test // #1646, #1718
|
||||
@@ -171,6 +179,29 @@ public abstract class IndexOperationsIntegrationTests {
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateIndexWithAliases() {
|
||||
// Given
|
||||
indexNameProvider.increment();
|
||||
String indexName = indexNameProvider.indexName();
|
||||
indexOperations = operations.indexOps(EntityWithAliases.class);
|
||||
indexOperations.createWithMapping();
|
||||
|
||||
// When
|
||||
Map<String, Set<AliasData>> aliases = indexOperations.getAliasesForIndex(indexName);
|
||||
|
||||
// Then
|
||||
AliasData result = aliases.values().stream().findFirst().orElse(new HashSet<>()).stream().findFirst().orElse(null);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getAlias()).isEqualTo("first_alias");
|
||||
assertThat(result.getFilterQuery()).asInstanceOf(InstanceOfAssertFactories.type(StringQuery.class))
|
||||
.extracting(StringQuery::getSource)
|
||||
.asString()
|
||||
.contains(Queries.wrapperQuery("""
|
||||
{"bool" : {"must" : {"term" : {"type" : "abc"}}}}
|
||||
""").query());
|
||||
}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
@Setting(settingPath = "settings/test-settings.json")
|
||||
@Mapping(mappingPath = "mappings/test-mappings.json")
|
||||
@@ -186,4 +217,33 @@ public abstract class IndexOperationsIntegrationTests {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}", aliases = {
|
||||
@Alias(value = "first_alias", filter = @Filter("""
|
||||
{"bool" : {"must" : {"term" : {"type" : "abc"}}}}
|
||||
"""))
|
||||
})
|
||||
private static class EntityWithAliases {
|
||||
@Nullable private @Id String id;
|
||||
@Nullable
|
||||
@Field(type = Text) private String type;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -58,6 +58,8 @@ import org.springframework.lang.Nullable;
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Brian Kimmig
|
||||
* @author Morgan Lutz
|
||||
* @author Haibo Liu
|
||||
* @author Andriy Redko
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
public abstract class MappingBuilderIntegrationTests extends MappingContextBaseTests {
|
||||
@@ -76,6 +78,12 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
|
||||
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSupportAllTypes() {
|
||||
IndexOperations indexOperations = operations.indexOps(EntityWithAllTypes.class);
|
||||
indexOperations.createWithMapping();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotFailOnCircularReference() {
|
||||
|
||||
@@ -908,7 +916,7 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
|
||||
@Nullable
|
||||
@Id private String id;
|
||||
|
||||
@Field(type = FieldType.Dense_Vector, dims = 42, similarity = "cosine") private double[] denseVector;
|
||||
@Field(type = FieldType.Dense_Vector, dims = 42, knnSimilarity = KnnSimilarity.COSINE) private double[] denseVector;
|
||||
}
|
||||
|
||||
@Mapping(aliases = {
|
||||
|
||||
+227
-32
@@ -62,6 +62,8 @@ import org.springframework.lang.Nullable;
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Brian Kimmig
|
||||
* @author Morgan Lutz
|
||||
* @author Haibo Liu
|
||||
* @author Andriy Redko
|
||||
*/
|
||||
public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
|
||||
@@ -695,6 +697,32 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
assertEquals(expected, mapping, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should write dense_vector properties for knn search")
|
||||
void shouldWriteDenseVectorPropertiesWithKnnSearch() throws JSONException {
|
||||
String expected = """
|
||||
{
|
||||
"properties":{
|
||||
"my_vector":{
|
||||
"type":"dense_vector",
|
||||
"dims":16,
|
||||
"element_type":"float",
|
||||
"similarity":"dot_product",
|
||||
"index_options":{
|
||||
"type":"hnsw",
|
||||
"m":16,
|
||||
"ef_construction":100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String mapping = getMappingBuilder().buildPropertyMapping(DenseVectorEntityWithKnnSearch.class);
|
||||
|
||||
assertEquals(expected, mapping, false);
|
||||
}
|
||||
|
||||
@Test // #1370
|
||||
@DisplayName("should not write mapping when enabled is false on entity")
|
||||
void shouldNotWriteMappingWhenEnabledIsFalseOnEntity() throws JSONException {
|
||||
@@ -741,6 +769,14 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
.isInstanceOf(MappingException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should match confidence interval parameter for dense_vector type")
|
||||
void shouldMatchConfidenceIntervalParameterForDenseVectorType() {
|
||||
|
||||
assertThatThrownBy(() -> getMappingBuilder().buildPropertyMapping(DenseVectorMisMatchConfidenceIntervalClass.class))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test // #1711
|
||||
@DisplayName("should write typeHint entries")
|
||||
void shouldWriteTypeHintEntries() throws JSONException {
|
||||
@@ -1090,38 +1126,49 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
|
||||
String expected = """
|
||||
{
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"excluded-date": {
|
||||
"type": "date",
|
||||
"format": "date"
|
||||
},
|
||||
"nestedEntity": {
|
||||
"type": "nested",
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"excluded-text": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"_source": {
|
||||
"excludes": [
|
||||
"excluded-date",
|
||||
"nestedEntity.excluded-text"
|
||||
]
|
||||
}
|
||||
}
|
||||
"""; //
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"excluded-date": {
|
||||
"type": "date",
|
||||
"format": "date"
|
||||
},
|
||||
"nestedEntity": {
|
||||
"type": "nested",
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"excluded-text": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"excluded-multifield": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"_source": {
|
||||
"excludes": [
|
||||
"excluded-date",
|
||||
"nestedEntity.excluded-text",
|
||||
"excluded-multifield"
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"""; //
|
||||
|
||||
String mapping = getMappingBuilder().buildPropertyMapping(ExcludedFieldEntity.class);
|
||||
|
||||
@@ -1207,6 +1254,91 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
|
||||
assertEquals(expected, mapping, true);
|
||||
}
|
||||
|
||||
@Test // #2942
|
||||
@DisplayName("should use custom mapped name")
|
||||
void shouldUseCustomMappedName() throws JSONException {
|
||||
|
||||
var expected = """
|
||||
{
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"someText": {
|
||||
"type": "match_only_text"
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
String mapping = getMappingBuilder().buildPropertyMapping(FieldMappedNameEntity.class);
|
||||
|
||||
assertEquals(expected, mapping, true);
|
||||
}
|
||||
|
||||
@Test // #2942
|
||||
@DisplayName("should use custom mapped name for multifield")
|
||||
void shouldUseCustomMappedNameMultiField() throws JSONException {
|
||||
|
||||
var expected = """
|
||||
{
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"description": {
|
||||
"type": "match_only_text",
|
||||
"fields": {
|
||||
"lower_case": {
|
||||
"type": "constant_keyword",
|
||||
"normalizer": "lower_case_normalizer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
String mapping = getMappingBuilder().buildPropertyMapping(MultiFieldMappedNameEntity.class);
|
||||
|
||||
assertEquals(expected, mapping, true);
|
||||
}
|
||||
|
||||
@Test // #2952
|
||||
void shouldMapNullityParameters() throws JSONException {
|
||||
// Given
|
||||
String expected = """
|
||||
{
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"empty-field": {
|
||||
"type": "keyword",
|
||||
"null_value": "EMPTY",
|
||||
"fields": {
|
||||
"suffix": {
|
||||
"type": "keyword",
|
||||
"null_value": "EMPTY_TEXT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// When
|
||||
String result = getMappingBuilder().buildPropertyMapping(MultiFieldWithNullEmptyParameters.class);
|
||||
|
||||
// Then
|
||||
assertEquals(expected, result, true);
|
||||
}
|
||||
|
||||
// region entities
|
||||
|
||||
@Document(indexName = "ignore-above-index")
|
||||
@@ -2063,6 +2195,35 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class DenseVectorEntityWithKnnSearch {
|
||||
@Nullable
|
||||
@Id private String id;
|
||||
|
||||
@Nullable
|
||||
@Field(type = FieldType.Dense_Vector, dims = 16, elementType = FieldElementType.FLOAT,
|
||||
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100),
|
||||
knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float[] my_vector;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public float[] getMy_vector() {
|
||||
return my_vector;
|
||||
}
|
||||
|
||||
public void setMy_vector(@Nullable float[] my_vector) {
|
||||
this.my_vector = my_vector;
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(enabled = false)
|
||||
static class DisabledMappingEntity {
|
||||
@Nullable
|
||||
@@ -2115,6 +2276,12 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
}
|
||||
}
|
||||
|
||||
static class DenseVectorMisMatchConfidenceIntervalClass {
|
||||
@Field(type = Dense_Vector, dims = 16, elementType = FieldElementType.FLOAT,
|
||||
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, confidenceInterval = 0.95F),
|
||||
knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float[] dense_vector;
|
||||
}
|
||||
|
||||
static class DisabledMappingProperty {
|
||||
@Nullable
|
||||
@Id private String id;
|
||||
@@ -2395,6 +2562,10 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
excludeFromSource = true) private LocalDate excludedDate;
|
||||
@Nullable
|
||||
@Field(type = Nested) private NestedExcludedFieldEntity nestedEntity;
|
||||
@Nullable
|
||||
@MultiField(mainField = @Field(name = "excluded-multifield", type = Text, excludeFromSource = true), otherFields = {
|
||||
@InnerField(suffix = "keyword", type = Keyword)
|
||||
}) private String excludedMultifield;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -2431,5 +2602,29 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
@Nullable
|
||||
@Field(type = Text) private String otherText;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class FieldMappedNameEntity {
|
||||
@Nullable
|
||||
@Field(type = Text, mappedTypeName = "match_only_text") private String someText;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MultiFieldMappedNameEntity {
|
||||
@Nullable
|
||||
@MultiField(mainField = @Field(type = FieldType.Text, mappedTypeName = "match_only_text"),
|
||||
otherFields = { @InnerField(suffix = "lower_case",
|
||||
type = FieldType.Keyword, normalizer = "lower_case_normalizer",
|
||||
mappedTypeName = "constant_keyword") }) private String description;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MultiFieldWithNullEmptyParameters {
|
||||
@Nullable
|
||||
@MultiField(
|
||||
mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true),
|
||||
otherFields = {
|
||||
@InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List<String> emptyField;
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
+17
-10
@@ -70,8 +70,8 @@ public class MappingParametersTest extends MappingContextBaseTests {
|
||||
}
|
||||
|
||||
@Test // #1700
|
||||
@DisplayName("should not allow dims length greater than 2048 for dense_vector type")
|
||||
void shouldNotAllowDimsLengthGreaterThan2048ForDenseVectorType() {
|
||||
@DisplayName("should not allow dims length greater than 4096 for dense_vector type")
|
||||
void shouldNotAllowDimsLengthGreaterThan4096ForDenseVectorType() {
|
||||
ElasticsearchPersistentEntity<?> failEntity = elasticsearchConverter.get().getMappingContext()
|
||||
.getRequiredPersistentEntity(DenseVectorInvalidDimsClass.class);
|
||||
Annotation annotation = failEntity.getRequiredPersistentProperty("dense_vector").findAnnotation(Field.class);
|
||||
@@ -90,21 +90,28 @@ public class MappingParametersTest extends MappingContextBaseTests {
|
||||
}
|
||||
|
||||
static class AnnotatedClass {
|
||||
@Nullable @Field private String field;
|
||||
@Nullable @MultiField(mainField = @Field,
|
||||
@Nullable
|
||||
@Field private String field;
|
||||
@Nullable
|
||||
@MultiField(mainField = @Field,
|
||||
otherFields = { @InnerField(suffix = "test", type = FieldType.Text) }) private String mainField;
|
||||
@Nullable @Field(type = FieldType.Text, docValues = false) private String docValuesText;
|
||||
@Nullable @Field(type = FieldType.Nested, docValues = false) private String docValuesNested;
|
||||
@Nullable @Field(type = Object, enabled = true) private String enabledObject;
|
||||
@Nullable @Field(type = Object, enabled = false) private String disabledObject;
|
||||
@Nullable
|
||||
@Field(type = FieldType.Text, docValues = false) private String docValuesText;
|
||||
@Nullable
|
||||
@Field(type = FieldType.Nested, docValues = false) private String docValuesNested;
|
||||
@Nullable
|
||||
@Field(type = Object, enabled = true) private String enabledObject;
|
||||
@Nullable
|
||||
@Field(type = Object, enabled = false) private String disabledObject;
|
||||
}
|
||||
|
||||
static class InvalidEnabledFieldClass {
|
||||
@Nullable @Field(type = FieldType.Text, enabled = false) private String disabledObject;
|
||||
@Nullable
|
||||
@Field(type = FieldType.Text, enabled = false) private String disabledObject;
|
||||
}
|
||||
|
||||
static class DenseVectorInvalidDimsClass {
|
||||
@Field(type = Dense_Vector, dims = 2049) private float[] dense_vector;
|
||||
@Field(type = Dense_Vector, dims = 4097) private float[] dense_vector;
|
||||
}
|
||||
|
||||
static class DenseVectorMissingDimsClass {
|
||||
|
||||
+62
@@ -17,30 +17,37 @@ package org.springframework.data.elasticsearch.core.index;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.skyscreamer.jsonassert.JSONAssert.*;
|
||||
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
|
||||
import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Alias;
|
||||
import org.springframework.data.elasticsearch.annotations.DateFormat;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.Filter;
|
||||
import org.springframework.data.elasticsearch.annotations.Mapping;
|
||||
import org.springframework.data.elasticsearch.annotations.Setting;
|
||||
import org.springframework.data.elasticsearch.client.elc.Queries;
|
||||
import org.springframework.data.elasticsearch.core.IndexOperations;
|
||||
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.ReactiveIndexOperations;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -346,6 +353,34 @@ public abstract class ReactiveIndexOperationsIntegrationTests {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateIndexWithAliases() {
|
||||
// Given
|
||||
indexNameProvider.increment();
|
||||
String indexName = indexNameProvider.indexName();
|
||||
indexOperations = operations.indexOps(EntityWithAliases.class);
|
||||
blocking(indexOperations).createWithMapping();
|
||||
|
||||
// When
|
||||
|
||||
// Then
|
||||
indexOperations.getAliasesForIndex(indexName)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(aliases -> {
|
||||
AliasData result = aliases.values().stream().findFirst().orElse(new HashSet<>()).stream().findFirst()
|
||||
.orElse(null);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getAlias()).isEqualTo("first_alias");
|
||||
assertThat(result.getFilterQuery()).asInstanceOf(InstanceOfAssertFactories.type(StringQuery.class))
|
||||
.extracting(StringQuery::getSource)
|
||||
.asString()
|
||||
.contains(Queries.wrapperQuery("""
|
||||
{"bool" : {"must" : {"term" : {"type" : "abc"}}}}
|
||||
""").query());
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
@Setting(shards = 3, replicas = 2, refreshInterval = "4s")
|
||||
static class Entity {
|
||||
@@ -401,4 +436,31 @@ public abstract class ReactiveIndexOperationsIntegrationTests {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}", aliases = {
|
||||
@Alias(value = "first_alias", filter = @Filter("""
|
||||
{"bool" : {"must" : {"term" : {"type" : "abc"}}}}
|
||||
"""))
|
||||
})
|
||||
private static class EntityWithAliases {
|
||||
@Nullable private @Id String id;
|
||||
@Field(type = Text) private String type;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
@@ -22,6 +22,7 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -30,7 +31,10 @@ import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.DateFormat;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.InnerField;
|
||||
import org.springframework.data.elasticsearch.annotations.Mapping;
|
||||
import org.springframework.data.elasticsearch.annotations.MultiField;
|
||||
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -79,6 +83,41 @@ public class ReactiveMappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
assertEquals(expected, mapping, true);
|
||||
}
|
||||
|
||||
@Test // #2952
|
||||
void shouldMapNullityParameters() throws JSONException {
|
||||
// Given
|
||||
ReactiveMappingBuilder mappingBuilder = getReactiveMappingBuilder();
|
||||
String expected = """
|
||||
{
|
||||
"properties": {
|
||||
"_class": {
|
||||
"type": "keyword",
|
||||
"index": false,
|
||||
"doc_values": false
|
||||
},
|
||||
"empty-field": {
|
||||
"type": "keyword",
|
||||
"null_value": "EMPTY",
|
||||
"fields": {
|
||||
"suffix": {
|
||||
"type": "keyword",
|
||||
"null_value": "EMPTY_TEXT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// When
|
||||
String result = Mono
|
||||
.defer(() -> mappingBuilder.buildReactivePropertyMapping(MultiFieldWithNullEmptyParameters.class))
|
||||
.subscribeOn(Schedulers.parallel()).block();
|
||||
|
||||
// Then
|
||||
assertEquals(expected, result, true);
|
||||
}
|
||||
|
||||
// region entities
|
||||
@Document(indexName = "runtime-fields")
|
||||
@Mapping(runtimeFieldsPath = "/mappings/runtime-fields.json")
|
||||
@@ -88,5 +127,14 @@ public class ReactiveMappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
@Field(type = Date, format = DateFormat.epoch_millis, name = "@timestamp")
|
||||
@Nullable private Instant timestamp;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MultiFieldWithNullEmptyParameters {
|
||||
@Nullable
|
||||
@MultiField(
|
||||
mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true),
|
||||
otherFields = {
|
||||
@InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List<String> emptyField;
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2024 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.sql;
|
||||
|
||||
import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*;
|
||||
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.query.SqlQuery;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* Testing the reactive querying using SQL syntax.
|
||||
*
|
||||
* @author Youssef Aouichaoui
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
@ContextConfiguration(classes = { ReactiveSqlOperationsIntegrationTests.Config.class })
|
||||
@DisplayName("Using Elasticsearch SQL Reactive Client")
|
||||
public class ReactiveSqlOperationsIntegrationTests {
|
||||
@Autowired ReactiveElasticsearchOperations operations;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// create index
|
||||
blocking(operations.indexOps(EntityForSQL.class)).createWithMapping();
|
||||
|
||||
// add data
|
||||
operations
|
||||
.saveAll(List.of(EntityForSQL.builder().withViews(3).build(), EntityForSQL.builder().withViews(0).build()),
|
||||
EntityForSQL.class)
|
||||
.blockLast();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// delete index
|
||||
blocking(operations.indexOps(EntityForSQL.class)).delete();
|
||||
}
|
||||
|
||||
// begin configuration region
|
||||
@Configuration
|
||||
@Import({ ReactiveElasticsearchTemplateConfiguration.class })
|
||||
static class Config {}
|
||||
// end region
|
||||
|
||||
@Test // #2683
|
||||
void when_search_with_an_sql_query() {
|
||||
// Given
|
||||
SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build();
|
||||
|
||||
// When
|
||||
|
||||
// Then
|
||||
operations.search(query).as(StepVerifier::create).expectNextCount(1).verifyComplete();
|
||||
}
|
||||
|
||||
// begin region
|
||||
@Document(indexName = "entity_for_sql")
|
||||
static class EntityForSQL {
|
||||
@Id private String id;
|
||||
private final Integer views;
|
||||
|
||||
public EntityForSQL(EntityForSQL.Builder builder) {
|
||||
this.views = builder.views;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Integer getViews() {
|
||||
return views;
|
||||
}
|
||||
|
||||
public static EntityForSQL.Builder builder() {
|
||||
return new EntityForSQL.Builder();
|
||||
}
|
||||
|
||||
static class Builder {
|
||||
private Integer views = 0;
|
||||
|
||||
public EntityForSQL.Builder withViews(Integer views) {
|
||||
this.views = views;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityForSQL build() {
|
||||
return new EntityForSQL(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
// end region
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2024 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.sql;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.IndexOperations;
|
||||
import org.springframework.data.elasticsearch.core.query.SqlQuery;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* Testing the querying using SQL syntax.
|
||||
*
|
||||
* @author Youssef Aouichaoui
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
@ContextConfiguration(classes = { SqlOperationsIntegrationTests.Config.class })
|
||||
@DisplayName("Using Elasticsearch SQL Client")
|
||||
class SqlOperationsIntegrationTests {
|
||||
@Autowired ElasticsearchOperations operations;
|
||||
@Nullable IndexOperations indexOps;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// create index
|
||||
indexOps = operations.indexOps(EntityForSQL.class);
|
||||
indexOps.createWithMapping();
|
||||
|
||||
// add data
|
||||
operations.save(EntityForSQL.builder().withViews(3).build(), EntityForSQL.builder().withViews(0).build());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// delete index
|
||||
if (indexOps != null) {
|
||||
indexOps.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// begin configuration region
|
||||
@Configuration
|
||||
@Import({ ElasticsearchTemplateConfiguration.class })
|
||||
static class Config {}
|
||||
// end region
|
||||
|
||||
@Test // #2683
|
||||
void when_search_with_an_sql_query() {
|
||||
// Given
|
||||
SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build();
|
||||
|
||||
// When
|
||||
|
||||
// Then
|
||||
SqlResponse response = operations.search(query);
|
||||
assertNotNull(response);
|
||||
assertFalse(response.getRows().isEmpty());
|
||||
assertEquals(1, response.getRows().size());
|
||||
}
|
||||
|
||||
@Test // #2683
|
||||
void when_search_with_an_sql_query_that_has_aggregated_column() {
|
||||
// Given
|
||||
SqlQuery query = SqlQuery.builder("SELECT SUM(views) AS TOTAL FROM entity_for_sql").build();
|
||||
|
||||
// When
|
||||
|
||||
// Then
|
||||
SqlResponse response = operations.search(query);
|
||||
assertThat(response.getColumns()).first().extracting(SqlResponse.Column::name).isEqualTo("TOTAL");
|
||||
assertThat(response.getRows()).hasSize(1).first().extracting(row -> row.get(response.getColumns().get(0)))
|
||||
.hasToString("3");
|
||||
}
|
||||
|
||||
// begin region
|
||||
@Document(indexName = "entity_for_sql")
|
||||
static class EntityForSQL {
|
||||
@Id private String id;
|
||||
private final Integer views;
|
||||
|
||||
public EntityForSQL(Builder builder) {
|
||||
this.views = builder.views;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Integer getViews() {
|
||||
return views;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static class Builder {
|
||||
private Integer views = 0;
|
||||
|
||||
public Builder withViews(Integer views) {
|
||||
this.views = views;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityForSQL build() {
|
||||
return new EntityForSQL(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
// end region
|
||||
|
||||
}
|
||||
+1
-10
@@ -132,7 +132,7 @@ public class ClusterConnection implements ExtensionContext.Store.CloseableResour
|
||||
DockerImageName dockerImageName = getDockerImageName(testcontainersProperties);
|
||||
|
||||
ElasticsearchContainer elasticsearchContainer = new SpringDataElasticsearchContainer(dockerImageName)
|
||||
.withEnv(testcontainersProperties).withStartupTimeout(Duration.ofMinutes(2));
|
||||
.withEnv(testcontainersProperties).withStartupTimeout(Duration.ofMinutes(2)).withReuse(true);
|
||||
elasticsearchContainer.start();
|
||||
|
||||
return ClusterConnectionInfo.builder() //
|
||||
@@ -192,16 +192,7 @@ public class ClusterConnection implements ExtensionContext.Store.CloseableResour
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
if (clusterConnectionInfo != null && clusterConnectionInfo.getElasticsearchContainer() != null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Stopping container");
|
||||
}
|
||||
clusterConnectionInfo.getElasticsearchContainer().stop();
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("closed");
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpringDataElasticsearchContainer extends ElasticsearchContainer {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2024 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.repositories.knn;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
|
||||
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Haibo Liu
|
||||
* @since 5.4
|
||||
*/
|
||||
@ContextConfiguration(classes = { KnnSearchELCIntegrationTests.Config.class })
|
||||
public class KnnSearchELCIntegrationTests extends KnnSearchIntegrationTests {
|
||||
|
||||
@Configuration
|
||||
@Import({ ElasticsearchTemplateConfiguration.class })
|
||||
@EnableElasticsearchRepositories(
|
||||
basePackages = { "org.springframework.data.elasticsearch.repositories.knn" },
|
||||
considerNestedRepositories = true)
|
||||
static class Config {
|
||||
@Bean
|
||||
IndexNameProvider indexNameProvider() {
|
||||
return new IndexNameProvider("knn-repository");
|
||||
}
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2024 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.repositories.knn;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldElementType;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.KnnAlgorithmType;
|
||||
import org.springframework.data.elasticsearch.annotations.KnnIndexOptions;
|
||||
import org.springframework.data.elasticsearch.annotations.KnnSimilarity;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.SearchHit;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Haibo Liu
|
||||
* @since 5.4
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
public abstract class KnnSearchIntegrationTests {
|
||||
|
||||
@Autowired ElasticsearchOperations operations;
|
||||
@Autowired private IndexNameProvider indexNameProvider;
|
||||
@Autowired private VectorEntityRepository vectorEntityRepository;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
indexNameProvider.increment();
|
||||
operations.indexOps(VectorEntity.class).createWithMapping();
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.Order(java.lang.Integer.MAX_VALUE)
|
||||
void cleanup() {
|
||||
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete();
|
||||
}
|
||||
|
||||
private List<VectorEntity> createVectorEntities(int n) {
|
||||
List<VectorEntity> entities = new ArrayList<>();
|
||||
float increment = 1.0f / n;
|
||||
for (int i = 0; i < n; i++) {
|
||||
VectorEntity entity = new VectorEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setMessage("top" + (i + 1));
|
||||
|
||||
// The generated vector is always in the first quadrant, from the x-axis direction to the y-axis direction
|
||||
float[] vector = new float[] { 1.0f - i * increment, increment };
|
||||
entity.setVector(vector);
|
||||
entities.add(entity);
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnXAxisVector() {
|
||||
|
||||
// given
|
||||
List<VectorEntity> entities = createVectorEntities(5);
|
||||
vectorEntityRepository.saveAll(entities);
|
||||
List<Float> xAxisVector = List.of(100f, 0f);
|
||||
|
||||
// when
|
||||
NativeQuery query = new NativeQueryBuilder()
|
||||
.withKnnSearches(ksb -> ksb.queryVector(xAxisVector).k(3).field("vector"))
|
||||
.withPageable(Pageable.ofSize(2))
|
||||
.build();
|
||||
SearchHits<VectorEntity> result = operations.search(query, VectorEntity.class);
|
||||
|
||||
List<VectorEntity> vectorEntities = result.getSearchHits().stream().map(SearchHit::getContent).toList();
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getTotalHits()).isEqualTo(3L);
|
||||
// should return the first vector, because it's near x-axis
|
||||
assertThat(vectorEntities.get(0).getMessage()).isEqualTo("top1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnYAxisVector() {
|
||||
|
||||
// given
|
||||
List<VectorEntity> entities = createVectorEntities(10);
|
||||
vectorEntityRepository.saveAll(entities);
|
||||
List<Float> yAxisVector = List.of(0f, 100f);
|
||||
|
||||
// when
|
||||
NativeQuery query = new NativeQueryBuilder()
|
||||
.withKnnSearches(ksb -> ksb.queryVector(yAxisVector).k(3).field("vector"))
|
||||
.withPageable(Pageable.ofSize(2))
|
||||
.build();
|
||||
SearchHits<VectorEntity> result = operations.search(query, VectorEntity.class);
|
||||
|
||||
List<VectorEntity> vectorEntities = result.getSearchHits().stream().map(SearchHit::getContent).toList();
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getTotalHits()).isEqualTo(3L);
|
||||
// should return the last vector, because it's near y-axis
|
||||
assertThat(vectorEntities.get(0).getMessage()).isEqualTo("top10");
|
||||
}
|
||||
|
||||
public interface VectorEntityRepository extends ElasticsearchRepository<VectorEntity, String> {}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
static class VectorEntity {
|
||||
@Nullable
|
||||
@Id private String id;
|
||||
|
||||
@Nullable
|
||||
@Field(type = Keyword) private String message;
|
||||
|
||||
@Field(type = FieldType.Dense_Vector, dims = 2, elementType = FieldElementType.FLOAT,
|
||||
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100),
|
||||
knnSimilarity = KnnSimilarity.COSINE) private float[] vector;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(@Nullable String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public float[] getVector() {
|
||||
return vector;
|
||||
}
|
||||
|
||||
public void setVector(@Nullable float[] vector) {
|
||||
this.vector = vector;
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -15,14 +15,22 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.repository.support;
|
||||
|
||||
import co.elastic.clients.elasticsearch.core.search.FieldCollapse;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
|
||||
import org.springframework.data.elasticsearch.client.elc.Queries;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
|
||||
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
|
||||
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
@@ -51,4 +59,33 @@ public class SimpleReactiveElasticsearchRepositoryELCIntegrationTests
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* search_after is used by the reactive search operation, it normally always adds _shard_doc as a tiebreaker sort
|
||||
* parameter. This must not be done when a collapse field is used as sort field, as in that case the collapse field
|
||||
* must be the only sort field.
|
||||
*/
|
||||
@Test // #2935
|
||||
@DisplayName("should use collapse_field for search_after in pit search")
|
||||
void shouldUseCollapseFieldForSearchAfterI() {
|
||||
var entity = new SampleEntity();
|
||||
entity.setId("42");
|
||||
entity.setMessage("m");
|
||||
entity.setKeyword("kw");
|
||||
repository.save(entity).block();
|
||||
|
||||
var query = NativeQuery.builder()
|
||||
.withQuery(Queries.matchAllQueryAsQuery())
|
||||
.withPageable(Pageable.unpaged())
|
||||
.withFieldCollapse(FieldCollapse.of(fcb -> fcb
|
||||
.field("keyword")))
|
||||
.withSort(Sort.by("keyword"))
|
||||
.build();
|
||||
|
||||
operations.search(query, SampleEntity.class)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
org.springframework.data.elasticsearch.blockhound.BlockHoundIntegrationCustomizer
|
||||
@@ -15,7 +15,7 @@
|
||||
#
|
||||
#
|
||||
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
|
||||
sde.testcontainers.image-version=8.13.2
|
||||
sde.testcontainers.image-version=8.15.5
|
||||
#
|
||||
#
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user