getFailedDocuments() {
- return failedDocuments;
- }
-}
diff --git a/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java b/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java
new file mode 100644
index 0000000000..c1eab9bcf7
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019-2025 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;
+
+import org.springframework.dao.NonTransientDataAccessResourceException;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class NoSuchIndexException extends NonTransientDataAccessResourceException {
+
+ private final String index;
+
+ /**
+ * @since 4.4
+ */
+ public NoSuchIndexException(String index) {
+ super(String.format("Index %s not found.", index));
+ this.index = index;
+ }
+
+ public NoSuchIndexException(String index, Throwable cause) {
+ super(String.format("Index %s not found.", index), cause);
+ this.index = index;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/ResourceFailureException.java b/src/main/java/org/springframework/data/elasticsearch/ResourceFailureException.java
new file mode 100644
index 0000000000..493d3b4b7b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/ResourceFailureException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021-2025 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;
+
+import org.springframework.core.NestedRuntimeException;
+
+/**
+ * @author Peter-Josef Meisch
+ */
+public class ResourceFailureException extends NestedRuntimeException {
+ public ResourceFailureException(String msg) {
+ super(msg);
+ }
+
+ public ResourceFailureException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/ResourceNotFoundException.java b/src/main/java/org/springframework/data/elasticsearch/ResourceNotFoundException.java
new file mode 100644
index 0000000000..5e97b4e00b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/ResourceNotFoundException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022-2025 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;
+
+import org.springframework.dao.NonTransientDataAccessResourceException;
+
+/**
+ * @author Peter-Josef Meisch
+ * @since 5.1
+ */
+public class ResourceNotFoundException extends NonTransientDataAccessResourceException {
+
+ public ResourceNotFoundException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/RestStatusException.java b/src/main/java/org/springframework/data/elasticsearch/RestStatusException.java
new file mode 100644
index 0000000000..c707686098
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/RestStatusException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021-2025 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;
+
+import org.springframework.dao.DataAccessException;
+
+/**
+ * Exception class for REST status exceptions independent from the used client/backend.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.3
+ */
+public class RestStatusException extends DataAccessException {
+
+ // we do not use a dedicated status class from Elasticsearch, OpenSearch, Spring web or webflux here
+ private final int status;
+
+ public RestStatusException(int status, String msg) {
+ super(msg);
+ this.status = status;
+ }
+
+ public RestStatusException(int status, String msg, Throwable cause) {
+ super(msg, cause);
+ this.status = status;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return "RestStatusException{" + "status=" + status + "} " + super.toString();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/UncategorizedElasticsearchException.java b/src/main/java/org/springframework/data/elasticsearch/UncategorizedElasticsearchException.java
new file mode 100644
index 0000000000..ffc71ef7ba
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/UncategorizedElasticsearchException.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020-2025 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;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.dao.UncategorizedDataAccessException;
+
+/**
+ * @author Peter-Josef Meisch
+ * @since 4.0
+ */
+public class UncategorizedElasticsearchException extends UncategorizedDataAccessException {
+
+ /**
+ * the response status code from Elasticsearch if available
+ *
+ * @since 4.4
+ */
+ @Nullable private final Integer statusCode;
+
+ /**
+ * The response body from Elasticsearch if available
+ *
+ * @since 4.4
+ */
+ @Nullable final String responseBody;
+
+ public UncategorizedElasticsearchException(String msg) {
+ this(msg, null);
+ }
+
+ public UncategorizedElasticsearchException(String msg, @Nullable Throwable cause) {
+ this(msg, null, null, cause);
+ }
+
+ public UncategorizedElasticsearchException(String msg, @Nullable Integer statusCode, @Nullable String responseBody,
+ @Nullable Throwable cause) {
+ super(msg, cause);
+ this.statusCode = statusCode;
+ this.responseBody = responseBody;
+ }
+
+ /**
+ * @since 4.4
+ */
+ @Nullable
+ public Integer getStatusCode() {
+ return statusCode;
+ }
+
+ /**
+ * @since 4.4
+ */
+ @Nullable
+ public String getResponseBody() {
+ return responseBody;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/VersionConflictException.java b/src/main/java/org/springframework/data/elasticsearch/VersionConflictException.java
new file mode 100644
index 0000000000..b3f31d3550
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/VersionConflictException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023-2025 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;
+
+import org.springframework.dao.DataIntegrityViolationException;
+
+/**
+ * Exception that is thrown when a version conflict from the server is detected.
+ *
+ * @author Peter-Josef Meisch
+ * @since 5.2
+ */
+public class VersionConflictException extends DataIntegrityViolationException {
+ public VersionConflictException(String msg) {
+ super(msg);
+ }
+
+ public VersionConflictException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java
new file mode 100644
index 0000000000..0f707e942e
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024-2025 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;
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java
new file mode 100644
index 0000000000..ea5d895294
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024-2025 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();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContext.java b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContext.java
new file mode 100644
index 0000000000..da27c1245b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContext.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019-2025 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.Documented;
+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;
+
+/**
+ * Based on reference doc - https://www.elastic.co/guide/en/elasticsearch/reference/current/suggester-context.html
+ *
+ * @author Robert Gruendler
+ * @author Peter-Josef Meisch
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+@Inherited
+public @interface CompletionContext {
+
+ String name();
+
+ ContextMappingType type();
+
+ String precision() default "";
+
+ String path() default "";
+
+ /**
+ * @since 4.3
+ */
+ enum ContextMappingType {
+ CATEGORY("category"), GEO("geo");
+
+ private final String mappedName;
+
+ ContextMappingType(String mappedName) {
+ this.mappedName = mappedName;
+ }
+
+ public String getMappedName() {
+ return mappedName;
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContextType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContextType.java
new file mode 100644
index 0000000000..0666accfc6
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContextType.java
@@ -0,0 +1,12 @@
+package org.springframework.data.elasticsearch.annotations;
+
+/**
+ * Based on reference doc - https://www.elastic.co/guide/en/elasticsearch/reference/current/suggester-context.html
+ *
+ * @author Robert Gruendler
+ */
+public enum CompletionContextType {
+
+ CATEGORY, GEO
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java
index 559ce55080..94ca1ea724 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,28 +15,36 @@
*/
package org.springframework.data.elasticsearch.annotations;
-import java.lang.annotation.*;
+import java.lang.annotation.Documented;
+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;
/**
- * Based on the reference doc - http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters-completion.html
+ * Based on the reference doc -
+ * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html
*
* @author Mewes Kochheim
+ * @author Robert Gruendler
+ * @author Peter-Josef Meisch
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
+@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Documented
@Inherited
public @interface CompletionField {
String searchAnalyzer() default "simple";
- String indexAnalyzer() default "simple";
-
- boolean payloads() default false;
+ String analyzer() default "simple";
boolean preserveSeparators() default true;
boolean preservePositionIncrements() default true;
int maxInputLength() default 50;
+
+ CompletionContext[] contexts() default {};
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/CountQuery.java b/src/main/java/org/springframework/data/elasticsearch/annotations/CountQuery.java
new file mode 100644
index 0000000000..80bb7c15f9
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/CountQuery.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * Alias for a @Query annotation with the count parameter set to true.
+ *
+ * @author Peter-Josef Meisch
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
+@Documented
+@Query(count = true)
+public @interface CountQuery {
+
+ @AliasFor(annotation = Query.class)
+ String value() default "";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java b/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java
index 056dc14822..9f3b7f9d78 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2014-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -16,16 +16,207 @@
package org.springframework.data.elasticsearch.annotations;
/**
+ * Values based on Elasticsearch reference
+ * documentation. The patterns are taken from this documentation and slightly adapted so that a Java
+ * {@link java.time.format.DateTimeFormatter} produces the same values as the Elasticsearch formatter. Use
+ * format = {}
to disable built-in date formats in the {@link Field} annotation. If you want to use only a
+ * custom date format pattern, you must set the format
property to empty {}
.
+ *
* @author Jakub Vavrik
- * Values based on reference doc - http://www.elasticsearch.org/guide/reference/mapping/date-format/
+ * @author Tim te Beek
+ * @author Peter-Josef Meisch
+ * @author Sascha Woo
*/
public enum DateFormat {
- none, custom, basic_date, basic_date_time, basic_date_time_no_millis, basic_ordinal_date, basic_ordinal_date_time,
- basic_ordinal_date_time_no_millis, basic_time, basic_time_no_millis, basic_t_time, basic_t_time_no_millis,
- basic_week_date, basic_week_date_time, basic_week_date_time_no_millis, date, date_hour, date_hour_minute,
- date_hour_minute_second, date_hour_minute_second_fraction, date_hour_minute_second_millis, date_optional_time,
- date_time, date_time_no_millis, hour, hour_minute, hour_minute_second, hour_minute_second_fraction,
- hour_minute_second_millis, ordinal_date, ordinal_date_time, ordinal_date_time_no_millis, time, time_no_millis,
- t_time, t_time_no_millis, week_date, week_date_time, weekDateTimeNoMillis, week_year, weekyearWeek,
- weekyearWeekDay, year, year_month, year_month_day
+ basic_date("uuuuMMdd"), //
+ basic_date_time("uuuuMMdd'T'HHmmss.SSSXXX"), //
+ basic_date_time_no_millis("uuuuMMdd'T'HHmmssXXX"), //
+ basic_ordinal_date("uuuuDDD"), //
+ basic_ordinal_date_time("yyyyDDD'T'HHmmss.SSSXXX"), //
+ basic_ordinal_date_time_no_millis("yyyyDDD'T'HHmmssXXX"), //
+ basic_time("HHmmss.SSSXXX"), //
+ basic_time_no_millis("HHmmssXXX"), //
+ basic_t_time("'T'HHmmss.SSSXXX"), //
+ basic_t_time_no_millis("'T'HHmmssXXX"), //
+ basic_week_date("YYYY'W'wwe"), // week-based-year!
+ /**
+ * @since 5.3
+ */
+ strict_basic_week_date("YYYY'W'wwe"), // week-based-year!
+ basic_week_date_time("YYYY'W'wwe'T'HHmmss.SSSX"), // here Elasticsearch uses a different zone format
+ /**
+ * @since 5.3
+ */
+ strict_basic_week_date_time("YYYY'W'wwe'T'HHmmss.SSSX"), // here Elasticsearch uses a different zone format
+ basic_week_date_time_no_millis("YYYY'W'wwe'T'HHmmssX"), //
+ /**
+ * @since 5.3
+ */
+ strict_basic_week_date_time_no_millis("YYYY'W'wwe'T'HHmmssX"), //
+ date("uuuu-MM-dd"), //
+ /**
+ * @since 5.3
+ */
+ strict_date("uuuu-MM-dd"), //
+ date_hour("uuuu-MM-dd'T'HH"), //
+ /**
+ * @since 5.3
+ */
+ strict_date_hour("uuuu-MM-dd'T'HH"), //
+ date_hour_minute("uuuu-MM-dd'T'HH:mm"), //
+ /**
+ * @since 5.3
+ */
+ strict_date_hour_minute("uuuu-MM-dd'T'HH:mm"), //
+ date_hour_minute_second("uuuu-MM-dd'T'HH:mm:ss"), //
+ /**
+ * @since 5.3
+ */
+ strict_date_hour_minute_second("uuuu-MM-dd'T'HH:mm:ss"), //
+ date_hour_minute_second_fraction("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
+ /**
+ * @since 5.3
+ */
+ strict_date_hour_minute_second_fraction("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
+ date_hour_minute_second_millis("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
+ /**
+ * @since 5.3
+ */
+ strict_date_hour_minute_second_millis("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
+ date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), //
+ /**
+ * @since 5.3
+ */
+ strict_date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), //
+ strict_date_optional_time_nanos("uuuu-MM-dd['T'HH:mm:ss.SSSSSSXXX]"), //
+ date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), //
+ date_time_no_millis("uuuu-MM-dd'T'HH:mm:ssVV"), // here Elasticsearch uses the zone-id in its implementation
+ /**
+ * @since 5.3
+ */
+ strict_date_time_no_millis("uuuu-MM-dd'T'HH:mm:ssVV"), // here Elasticsearch uses the zone-id in its implementation
+ epoch_millis("epoch_millis"), //
+ epoch_second("epoch_second"), //
+ hour("HH"), //
+ /**
+ * @since 5.3
+ */
+ strict_hour("HH"), //
+ hour_minute("HH:mm"), //
+ /**
+ * @since 5.3
+ */
+ strict_hour_minute("HH:mm"), //
+ hour_minute_second("HH:mm:ss"), //
+ /**
+ * @since 5.3
+ */
+ strict_hour_minute_second("HH:mm:ss"), //
+ hour_minute_second_fraction("HH:mm:ss.SSS"), //
+ /**
+ * @since 5.3
+ */
+ strict_hour_minute_second_fraction("HH:mm:ss.SSS"), //
+ hour_minute_second_millis("HH:mm:ss.SSS"), //
+ /**
+ * @since 5.3
+ */
+ strict_hour_minute_second_millis("HH:mm:ss.SSS"), //
+ ordinal_date("uuuu-DDD"), //
+ /**
+ * @since 5.3
+ */
+ strict_ordinal_date("uuuu-DDD"), //
+ ordinal_date_time("uuuu-DDD'T'HH:mm:ss.SSSXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_ordinal_date_time("uuuu-DDD'T'HH:mm:ss.SSSXXX"), //
+ ordinal_date_time_no_millis("uuuu-DDD'T'HH:mm:ssXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_ordinal_date_time_no_millis("uuuu-DDD'T'HH:mm:ssXXX"), //
+ time("HH:mm:ss.SSSXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_time("HH:mm:ss.SSSXXX"), //
+ time_no_millis("HH:mm:ssXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_time_no_millis("HH:mm:ssXXX"), //
+ t_time("'T'HH:mm:ss.SSSXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_t_time("'T'HH:mm:ss.SSSXXX"), //
+ t_time_no_millis("'T'HH:mm:ssXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_t_time_no_millis("'T'HH:mm:ssXXX"), //
+ week_date("YYYY-'W'ww-e"), //
+ /**
+ * @since 5.3
+ */
+ strict_week_date("YYYY-'W'ww-e"), //
+ week_date_time("YYYY-'W'ww-e'T'HH:mm:ss.SSSXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_week_date_time("YYYY-'W'ww-e'T'HH:mm:ss.SSSXXX"), //
+ week_date_time_no_millis("YYYY-'W'ww-e'T'HH:mm:ssXXX"), //
+ /**
+ * @since 5.3
+ */
+ strict_week_date_time_no_millis("YYYY-'W'ww-e'T'HH:mm:ssXXX"), //
+ weekyear(""), // no TemporalAccessor available for these 3
+ /**
+ * @since 5.3
+ */
+ strict_weekyear(""), // no TemporalAccessor available for these 3
+ weekyear_week(""), //
+ /**
+ * @since 5.3
+ */
+ strict_weekyear_week(""), //
+ weekyear_week_day(""), //
+ /**
+ * @since 5.3
+ */
+ strict_strict_weekyear_week_day(""), //
+ year("uuuu"), //
+ /**
+ * @since 5.3
+ */
+ strict_year("uuuu"), //
+ year_month("uuuu-MM"), //
+ /**
+ * @since 5.3
+ */
+ strict_year_month("uuuu-MM"), //
+ year_month_day("uuuu-MM-dd"), //
+ /**
+ * @since 5.3
+ */
+ strict_year_month_day("uuuu-MM-dd"); //
+
+ private final String pattern;
+
+ DateFormat(String pattern) {
+ this.pattern = pattern;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public String getPattern() {
+ return pattern;
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java
index ef32e7e386..1131b2cd59 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java
@@ -1,46 +1,132 @@
-/*
- * Copyright 2013 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
- *
- * http://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.*;
-
-import org.springframework.data.annotation.Persistent;
-
-/**
- * Document
- *
- * @author Rizwan Idrees
- * @author Mohsin Husen
- */
-
-@Persistent
-@Inherited
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE})
-public @interface Document {
-
- String indexName();
-
- String type() default "";
-
- short shards() default 5;
-
- short replicas() default 1;
-
- String refreshInterval() default "1s";
-
- String indexStoreType() default "fs";
-}
+/*
+ * Copyright 2013-2025 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;
+
+import org.springframework.data.annotation.Persistent;
+
+/**
+ * Identifies a domain object to be persisted to Elasticsearch.
+ *
+ * @author Rizwan Idrees
+ * @author Mohsin Husen
+ * @author Mason Chan
+ * @author Ivan Greene
+ * @author Mark Paluch
+ * @author Peter-Josef Meisch
+ * @author Sascha Woo
+ */
+@Persistent
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE })
+public @interface Document {
+
+ /**
+ * Name of the Elasticsearch index.
+ *
+ * - Lowercase only
+ * - Cannot include \, /, *, ?, ", >, <, |, ` ` (space character), ,, #
+ * - Cannot start with -, _, +
+ * - Cannot be . or ..
+ * - Cannot be longer than 255 bytes (note it is bytes, so multi-byte characters will count towards the 255 limit
+ * faster)
+ *
+ */
+ String indexName();
+
+ /**
+ * Configuration whether to create an index on repository bootstrapping.
+ */
+ boolean createIndex() default true;
+
+ /**
+ * If true, the index mapping will be written on repository bootstrapping even when the index already exists. This
+ * allows for automatically updating the mapping with new properties. Changes on existing properties will lead to an
+ * error from the Elasticsearch server.
+ */
+ boolean alwaysWriteMapping() default false;
+
+ /**
+ * Configuration of version management.
+ */
+ VersionType versionType() default VersionType.EXTERNAL;
+
+ /**
+ * Defines if type hints should be written. {@see WriteTypeHint}.
+ *
+ * @since 4.3
+ */
+ WriteTypeHint writeTypeHint() default WriteTypeHint.DEFAULT;
+
+ /**
+ * Controls how Elasticsearch dynamically adds fields to the document.
+ *
+ * @since 4.3
+ */
+ Dynamic dynamic() default Dynamic.INHERIT;
+
+ /**
+ * Specifies if the id property should also be stored in the Elasticsearch document source. Default value is
+ * {@literal true}
+ *
+ * @since 5.1
+ */
+ boolean storeIdInSource() default true;
+
+ /**
+ * Specifies if the version property should also be stored in the Elasticsearch document source. Default value is
+ * true.
+ *
+ * @since 5.1
+ */
+ boolean storeVersionInSource() default true;
+
+ /**
+ * Aliases for the index.
+ *
+ * @since 5.4
+ */
+ Alias[] aliases() default {};
+
+ /**
+ * @since 4.3
+ */
+ enum VersionType {
+ INTERNAL("internal"), //
+ EXTERNAL("external"), //
+ EXTERNAL_GTE("external_gte"), //
+ /**
+ * @since 4.4
+ */
+ FORCE("force");
+
+ private final String esName;
+
+ VersionType(String esName) {
+ this.esName = esName;
+ }
+
+ public String getEsName() {
+ return esName;
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java
new file mode 100644
index 0000000000..9868c6e3c6
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021-2025 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;
+
+/**
+ * Values for the {@code dynamic} mapping parameter.
+ *
+ * @author Sascha Woo
+ * @since 4.3
+ */
+public enum Dynamic {
+ /**
+ * New fields are added to the mapping.
+ */
+ TRUE("true"),
+ /**
+ * New fields are added to the mapping as
+ * runtime fields. These
+ * fields are not indexed, and are loaded from {@code _source} at query time.
+ */
+ RUNTIME("runtime"),
+ /**
+ * New fields are ignored. These fields will not be indexed or searchable, but will still appear in the
+ * {@code _source} field of returned hits. These fields will not be added to the mapping, and new fields must be added
+ * explicitly.
+ */
+ FALSE("false"),
+ /**
+ * If new fields are detected, an exception is thrown and the document is rejected. New fields must be explicitly
+ * added to the mapping.
+ */
+ STRICT("strict"),
+ /**
+ * Inherit the dynamic setting from their parent object or from the mapping type.
+ */
+ INHERIT("inherit");
+
+ private final String mappedName;
+
+ Dynamic(String mappedName) {
+ this.mappedName = mappedName;
+ }
+
+ public String getMappedName() {
+ return mappedName;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicTemplates.java b/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicTemplates.java
new file mode 100644
index 0000000000..107ee94be2
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicTemplates.java
@@ -0,0 +1,25 @@
+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;
+
+import org.springframework.data.annotation.Persistent;
+
+/**
+ * Elasticsearch dynamic templates mapping. This annotation is handy if you prefer apply dynamic templates on fields
+ * with annotation e.g. {@link Field} with type = FieldType.Object etc. instead of static mapping on Document via
+ * {@link Mapping} annotation. DynamicTemplates annotation is omitted if {@link Mapping} annotation is used.
+ *
+ * @author Petr Kukral
+ */
+@Persistent
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE })
+public @interface DynamicTemplates {
+
+ String mappingPath() default "";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java
index f4cec2b380..97815477ae 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java
@@ -1,56 +1,255 @@
-/*
- * Copyright 2013 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
- *
- * http://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.Documented;
-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;
-
-/**
- * @author Rizwan Idrees
- * @author Mohsin Husen
- * @author Artur Konczak
- * @author Jonathan Yan
- * @author Jakub Vavrik
- * @author Kevin Leturc
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
-@Documented
-@Inherited
-public @interface Field {
-
- FieldType type() default FieldType.Auto;
-
- FieldIndex index() default FieldIndex.analyzed;
-
- DateFormat format() default DateFormat.none;
-
- String pattern() default "";
-
- boolean store() default false;
-
- String searchAnalyzer() default "";
-
- String indexAnalyzer() default "";
-
- String[] ignoreFields() default {};
-
- boolean includeInParent() default false;
-}
+/*
+ * Copyright 2013-2025 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.Documented;
+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;
+
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * @author Rizwan Idrees
+ * @author Mohsin Husen
+ * @author Artur Konczak
+ * @author Jonathan Yan
+ * @author Jakub Vavrik
+ * @author Kevin Leturc
+ * @author Peter-Josef Meisch
+ * @author Xiao Yu
+ * @author Aleksei Arsenev
+ * @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 })
+@Documented
+@Inherited
+public @interface Field {
+
+ /**
+ * Alias for {@link #name}.
+ *
+ * @since 3.2
+ */
+ @AliasFor("name")
+ String value() default "";
+
+ /**
+ * The name to be used to store the field inside the document. If not set, the name of the annotated property
+ * is used.
+ *
+ * @since 3.2
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ FieldType type() default FieldType.Auto;
+
+ boolean index() default true;
+
+ DateFormat[] format() default { DateFormat.date_optional_time, DateFormat.epoch_millis };
+
+ String[] pattern() default {};
+
+ boolean store() default false;
+
+ boolean fielddata() default false;
+
+ String searchAnalyzer() default "";
+
+ String analyzer() default "";
+
+ String normalizer() default "";
+
+ String[] ignoreFields() default {};
+
+ boolean includeInParent() default false;
+
+ String[] copyTo() default {};
+
+ /**
+ * @since 4.0
+ */
+ int ignoreAbove() default -1;
+
+ /**
+ * @since 4.0
+ */
+ boolean coerce() default true;
+
+ /**
+ * @since 4.0
+ */
+ boolean docValues() default true;
+
+ /**
+ * @since 4.0
+ */
+ boolean ignoreMalformed() default false;
+
+ /**
+ * @since 4.0
+ */
+ IndexOptions indexOptions() default IndexOptions.none;
+
+ /**
+ * @since 4.0
+ */
+ boolean indexPhrases() default false;
+
+ /**
+ * implemented as array to enable the empty default value
+ *
+ * @since 4.0
+ */
+ IndexPrefixes[] indexPrefixes() default {};
+
+ /**
+ * @since 4.0
+ */
+ 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 "";
+
+ /**
+ * @since 4.0
+ */
+ int positionIncrementGap() default -1;
+
+ /**
+ * @since 4.0
+ */
+ String similarity() default Similarity.Default;
+
+ /**
+ * @since 4.0
+ */
+ TermVector termVector() default TermVector.none;
+
+ /**
+ * @since 4.0
+ */
+ double scalingFactor() default 1;
+
+ /**
+ * @since 4.0
+ */
+ int maxShingleSize() default -1;
+
+ /**
+ * if true, the field will be stored in Elasticsearch even if it has a null value
+ *
+ * @since 4.1
+ */
+ boolean storeNullValue() default false;
+
+ /**
+ * to be used in combination with {@link FieldType#Rank_Feature}
+ *
+ * @since 4.1
+ */
+ boolean positiveScoreImpact() default true;
+
+ /**
+ * to be used in combination with {@link FieldType#Object}
+ *
+ * @since 4.1
+ */
+ boolean enabled() default true;
+
+ /**
+ * @since 4.1
+ */
+ boolean eagerGlobalOrdinals() default false;
+
+ /**
+ * @since 4.1
+ */
+ NullValueType nullValueType() default NullValueType.String;
+
+ /**
+ * to be used in combination with {@link FieldType#Dense_Vector}
+ *
+ * @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 {};
+
+ /**
+ * Controls how Elasticsearch dynamically adds fields to the inner object within the document.
+ * To be used in combination with {@link FieldType#Object} or {@link FieldType#Nested}
+ *
+ * @since 4.3
+ */
+ Dynamic dynamic() default Dynamic.INHERIT;
+
+ /**
+ * marks this field to be excluded from the _source in Elasticsearch
+ * (https://www.elastic.co/guide/en/elasticsearch/reference/7.15/mapping-source-field.html#include-exclude)
+ *
+ * @since 4.3
+ */
+ boolean excludeFromSource() default false;
+
+ /**
+ * when this field is a {{@link String}}, a {{@link java.util.Collection}} or a {{@link java.util.Map}} that is empty
+ * this property controlls whether the empty value is sent to Elasticsearch.
+ *
+ * @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 "";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldElementType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldElementType.java
new file mode 100644
index 0000000000..49271764ba
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldElementType.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024-2025 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";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java
index d475397491..f701948d6d 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -19,7 +19,82 @@
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Artur Konczak
+ * @author Zeng Zetang
+ * @author Peter-Josef Meisch
+ * @author Aleksei Arsenev
+ * @author Brian Kimmig
+ * @author Morgan Lutz
*/
public enum FieldType {
- String, Integer, Long, Date, Float, Double, Boolean, Object, Auto, Nested, Ip
+ Auto("auto"), //
+ Text("text"), //
+ Keyword("keyword"), //
+ Long("long"), //
+ Integer("integer"), //
+ Short("short"), //
+ Byte("byte"), //
+ Double("double"), //
+ Float("float"), //
+ Half_Float("half_float"), //
+ Scaled_Float("scaled_float"), //
+ Date("date"), //
+ Date_Nanos("date_nanos"), //
+ Boolean("boolean"), //
+ Binary("binary"), //
+ Integer_Range("integer_range"), //
+ Float_Range("float_range"), //
+ Long_Range("long_range"), //
+ Double_Range("double_range"), //
+ Date_Range("date_range"), //
+ Ip_Range("ip_range"), //
+ Object("object"), //
+ Nested("nested"), //
+ Ip("ip"), //
+ TokenCount("token_count"), //
+ Percolator("percolator"), //
+ Flattened("flattened"), //
+ Search_As_You_Type("search_as_you_type"), //
+ /** @since 4.1 */
+ Rank_Feature("rank_feature"), //
+ /** @since 4.1 */
+ Rank_Features("rank_features"), //
+ /** since 4.2 */
+ Wildcard("wildcard"), //
+ /** @since 4.2 */
+ Dense_Vector("dense_vector"), //
+ /**
+ * @since 5.2
+ */
+ Constant_Keyword("constant_keyword"), //
+ /**
+ * @since 5.2
+ */
+ Alias("alias"), //
+ /**
+ * @since 5.2
+ */
+ Version("version"), //
+ /**
+ * @since 5.2
+ */
+ Murmur3("murmur3"), //
+ /**
+ * @since 5.2
+ */
+ Match_Only_Text("match_only_text"), //
+ /**
+ * @since 5.2
+ */
+ Annotated_Text("annotated_text") //
+ ;
+
+ private final String mappedName;
+
+ FieldType(String mappedName) {
+ this.mappedName = mappedName;
+ }
+
+ public String getMappedName() {
+ return mappedName;
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java
new file mode 100644
index 0000000000..7f07df55d1
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024-2025 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 "";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java
index bdb8025bb9..05695abc98 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java
new file mode 100644
index 0000000000..0121b07ee1
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Lukas Vorisek
+ * @author Peter-Josef Meisch
+ * @since 4.1
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+public @interface GeoShapeField {
+ Orientation orientation() default Orientation.ccw;
+
+ boolean ignoreMalformed() default false;
+
+ boolean ignoreZValue() default true;
+
+ boolean coerce() default false;
+
+ enum Orientation {
+ right, ccw, counterclockwise, left, cw, clockwise
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Highlight.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Highlight.java
new file mode 100644
index 0000000000..30312ab434
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Highlight.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Peter-Josef Meisch
+ * @since 4.0
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Highlight {
+
+ HighlightParameters parameters() default @HighlightParameters;
+
+ HighlightField[] fields();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightField.java
new file mode 100644
index 0000000000..f6318be98a
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightField.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020-2025 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.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author Peter-Josef Meisch
+ * @since 4.0
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+public @interface HighlightField {
+
+ /**
+ * The name of the field to apply highlighting to. This must be the field name of the entity's property, not the name
+ * of the field in the index mappings.
+ */
+ String name() default "";
+
+ HighlightParameters parameters() default @HighlightParameters;
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java
new file mode 100644
index 0000000000..d4e8bbfd2b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020-2025 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.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author Peter-Josef Meisch
+ * @author Haibo Liu
+ * @since 4.0
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+public @interface HighlightParameters {
+ String boundaryChars() default "";
+
+ int boundaryMaxScan() default -1;
+
+ String boundaryScanner() default "";
+
+ String boundaryScannerLocale() default "";
+
+ /**
+ * only used for {@link Highlight}s.
+ */
+ String encoder() default "";
+
+ boolean forceSource() default false;
+
+ String fragmenter() default "";
+
+ /**
+ * only used for {@link HighlightField}s.
+ */
+ int fragmentOffset() default -1;
+
+ int fragmentSize() default -1;
+
+ /**
+ * only used for {@link HighlightField}s.
+ */
+ String[] matchedFields() default {};
+
+ int noMatchSize() default -1;
+
+ int numberOfFragments() default -1;
+
+ Query highlightQuery() default @Query;
+
+ String order() default "";
+
+ int phraseLimit() default -1;
+
+ String[] preTags() default {};
+
+ String[] postTags() default {};
+
+ boolean requireFieldMatch() default true;
+
+ /**
+ * only used for {@link Highlight}s.
+ */
+ String tagsSchema() default "";
+
+ String type() default "";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldIndex.java b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexOptions.java
similarity index 69%
rename from src/main/java/org/springframework/data/elasticsearch/annotations/FieldIndex.java
rename to src/main/java/org/springframework/data/elasticsearch/annotations/IndexOptions.java
index e5642551a1..2de226c7b1 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldIndex.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexOptions.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2019-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -16,11 +16,9 @@
package org.springframework.data.elasticsearch.annotations;
/**
- * @author Artur Konczak
- * @author Mohsin Husen
- * @author Alexander Volz
- * @author Dennis Maaß
+ * @author Peter-Josef Meisch
+ * @since 4.0
*/
-public enum FieldIndex {
- not_analyzed, analyzed, no
+public enum IndexOptions {
+ none, docs, freqs, positions, offsets
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Parent.java b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexPrefixes.java
similarity index 62%
rename from src/main/java/org/springframework/data/elasticsearch/annotations/Parent.java
rename to src/main/java/org/springframework/data/elasticsearch/annotations/IndexPrefixes.java
index 940e6f04f8..01adc8fbb4 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/Parent.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexPrefixes.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2019-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,21 +15,15 @@
*/
package org.springframework.data.elasticsearch.annotations;
-import java.lang.annotation.*;
-
-import org.springframework.data.annotation.Persistent;
-
/**
- * Parent
- *
- * @author Philipp Jardas
+ * @author Peter-Josef Meisch
+ * @since 4.0
*/
+public @interface IndexPrefixes {
+ int MIN_DEFAULT = 2;
+ int MAX_DEFAULT = 2;
-@Persistent
-@Inherited
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
-public @interface Parent {
+ int minChars() default MIN_DEFAULT;
- String type();
+ int maxChars() default MAX_DEFAULT;
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java
new file mode 100644
index 0000000000..4d76b97492
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark a String property of an entity to be filled with the name of the index where the entity was stored
+ * after it is indexed into Elasticsearch. This can be used when the name of the index is dynamically created or when a
+ * document was indexed into a write alias.
+ *
+ * This can not be used to specify the index where an entity should be written to.
+ *
+ * @author Peter-Josef Meisch
+ * @since 5.1
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
+@Documented
+@Field(type = FieldType.Auto) // prevents the property being written to the index mapping
+public @interface IndexedIndexName {
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java
new file mode 100644
index 0000000000..651bf5a825
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2014-2025 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Artur Konczak
+ * @author Mohsin Husen
+ * @author Sascha Woo
+ * @author Xiao Yu
+ * @author Peter-Josef Meisch
+ * @author Aleksei Arsenev
+ * @author Brian Kimmig
+ * @author Morgan Lutz
+ * @author Haibo Liu
+ * @author Andriy Redko
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.ANNOTATION_TYPE)
+public @interface InnerField {
+
+ String suffix();
+
+ FieldType type();
+
+ boolean index() default true;
+
+ DateFormat[] format() default { DateFormat.date_optional_time, DateFormat.epoch_millis };
+
+ String[] pattern() default {};
+
+ boolean store() default false;
+
+ boolean fielddata() default false;
+
+ String searchAnalyzer() default "";
+
+ String analyzer() default "";
+
+ String normalizer() default "";
+
+ /**
+ * @since 4.0
+ */
+ int ignoreAbove() default -1;
+
+ /**
+ * @since 4.0
+ */
+ boolean coerce() default true;
+
+ /**
+ * @since 4.0
+ */
+ boolean docValues() default true;
+
+ /**
+ * @since 4.0
+ */
+ boolean ignoreMalformed() default false;
+
+ /**
+ * @since 4.0
+ */
+ IndexOptions indexOptions() default IndexOptions.none;
+
+ /**
+ * @since 4.0
+ */
+ boolean indexPhrases() default false;
+
+ /**
+ * implemented as array to enable the empty default value
+ *
+ * @since 4.0
+ */
+ IndexPrefixes[] indexPrefixes() default {};
+
+ /**
+ * @since 4.0
+ */
+ boolean norms() default true;
+
+ /**
+ * @since 4.0
+ */
+ String nullValue() default "";
+
+ /**
+ * @since 4.0
+ */
+ int positionIncrementGap() default -1;
+
+ /**
+ * @since 4.0
+ */
+ String similarity() default Similarity.Default;
+
+ /**
+ * @since 4.0
+ */
+ TermVector termVector() default TermVector.none;
+
+ /**
+ * @since 4.0
+ */
+ double scalingFactor() default 1;
+
+ /**
+ * @since 4.0
+ */
+ int maxShingleSize() default -1;
+
+ /**
+ * to be used in combination with {@link FieldType#Rank_Feature}
+ *
+ * @since 4.1
+ */
+ boolean positiveScoreImpact() default true;
+
+ /**
+ * @since 4.1
+ */
+ boolean eagerGlobalOrdinals() default false;
+
+ /**
+ * @since 4.1
+ */
+ NullValueType nullValueType() default NullValueType.String;
+
+ /**
+ * to be used in combination with {@link FieldType#Dense_Vector}
+ *
+ * @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 "";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelation.java b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelation.java
new file mode 100644
index 0000000000..eb2e1e4623
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelation.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Subhobrata Dey
+ * @since 4.1
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.ANNOTATION_TYPE)
+public @interface JoinTypeRelation {
+
+ String parent();
+
+ String[] children();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/NestedField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelations.java
similarity index 70%
rename from src/main/java/org/springframework/data/elasticsearch/annotations/NestedField.java
rename to src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelations.java
index 7a7f001f8c..2004200cf2 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/NestedField.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelations.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2020-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,27 +15,22 @@
*/
package org.springframework.data.elasticsearch.annotations;
+import java.lang.annotation.Documented;
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;
/**
- *
+ * @author Subhobrata Dey
+ * @since 4.1
*/
+@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
-public @interface NestedField {
-
- String dotSuffix();
-
- FieldType type();
-
- FieldIndex index() default FieldIndex.analyzed;
-
- boolean store() default false;
-
- String searchAnalyzer() default "";
+@Inherited
+public @interface JoinTypeRelations {
- String indexAnalyzer() default "";
+ JoinTypeRelation[] relations();
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/KnnAlgorithmType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnAlgorithmType.java
new file mode 100644
index 0000000000..6110e54be8
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnAlgorithmType.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024-2025 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;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/KnnIndexOptions.java b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnIndexOptions.java
new file mode 100644
index 0000000000..56d871d3b5
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnIndexOptions.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024-2025 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;
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/KnnSimilarity.java b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnSimilarity.java
new file mode 100644
index 0000000000..d03c42a6fd
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnSimilarity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024-2025 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;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java
index 122251ee65..c2d48c3884 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2014-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,20 +15,73 @@
*/
package org.springframework.data.elasticsearch.annotations;
-import java.lang.annotation.*;
+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;
+
import org.springframework.data.annotation.Persistent;
/**
* Elasticsearch Mapping
*
* @author Mohsin Husen
+ * @author Peter-Josef Meisch
*/
@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE})
+@Target({ ElementType.TYPE, ElementType.FIELD })
public @interface Mapping {
String mappingPath() default "";
+ /**
+ * whether mappings are enabled
+ *
+ * @since 4.2
+ */
+ boolean enabled() default true;
+
+ /**
+ * whether date_detection is enabled
+ *
+ * @since 4.3
+ */
+ Detection dateDetection() default Detection.DEFAULT;
+
+ /**
+ * whether numeric_detection is enabled
+ *
+ * @since 4.3
+ */
+ Detection numericDetection() default Detection.DEFAULT;
+
+ /**
+ * custom dynamic date formats
+ *
+ * @since 4.3
+ */
+ String[] dynamicDateFormats() default {};
+
+ /**
+ * classpath to a JSON file containing the values for a runtime mapping definition. The file must contain the JSON
+ * object that is written as the value of the runtime property. {@see elasticsearch doc}
+ *
+ * @since 4.3
+ */
+ String runtimeFieldsPath() default "";
+
+ /**
+ * field alias definitions to be written to the index mapping
+ *
+ * @since 5.3
+ */
+ MappingAlias[] aliases() default {};
+
+ enum Detection {
+ DEFAULT, TRUE, FALSE
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java b/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java
new file mode 100644
index 0000000000..791659e9d5
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024-2025 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.Documented;
+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;
+
+/**
+ * Defines a field alias in the index mapping.
+ *
+ * @author Peter-Josef Meisch
+ * @since 5.3
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+@Inherited
+public @interface MappingAlias {
+ /**
+ * the name of the alias.
+ */
+ String name();
+
+ /**
+ * the path of the alias.
+ */
+ String path();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java
index 41c781e80c..9dff38c1f1 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2014-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,20 +15,26 @@
*/
package org.springframework.data.elasticsearch.annotations;
-import java.lang.annotation.*;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
/**
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Artur Konczak
* @author Jonathan Yan
+ * @author Xiao Yu
+ * @author Peter-Josef Meisch
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
+@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Documented
public @interface MultiField {
- public Field mainField();
+ Field mainField();
- public NestedField[] otherFields() default {};
+ InnerField[] otherFields() default {};
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/NullValueType.java
similarity index 65%
rename from src/main/java/org/springframework/data/elasticsearch/core/facet/FacetType.java
rename to src/main/java/org/springframework/data/elasticsearch/annotations/NullValueType.java
index c913ee107f..a131b12a8e 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetType.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/NullValueType.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2020-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.data.elasticsearch.core.facet;
+package org.springframework.data.elasticsearch.annotations;
/**
- * @author Artur Konczak
- * @author Petar Tahchiev
+ * @author Peter-Josef Meisch
+ * @since 4.1
*/
-public enum FacetType {
-
- term, range, histogram, statistical
-
+public enum NullValueType {
+ String, Integer, Long, Double
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java
index 3c91cdf293..9f1b755c35 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java
@@ -1,45 +1,62 @@
-/*
- * Copyright 2013 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
- *
- * http://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.*;
-
-/**
- * Query
- *
- * @author Rizwan Idrees
- * @author Mohsin Husen
- */
-
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-@Documented
-public @interface Query {
-
- /**
- * Elasticsearch query to be used when executing query. May contain placeholders eg. ?0
- *
- * @return
- */
- String value() default "";
-
- /**
- * Named Query Named looked up by repository.
- *
- * @return
- */
- String name() default "";
-}
+/*
+ * Copyright 2013-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.data.annotation.QueryAnnotation;
+
+/**
+ * Query
+ *
+ * @author Rizwan Idrees
+ * @author Mohsin Husen
+ * @author Peter-Josef Meisch
+ * @author Steven Pearce
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
+@Documented
+@QueryAnnotation
+public @interface Query {
+
+ /**
+ * @return Elasticsearch query to be used when executing query. May contain placeholders eg. ?0. Alias for query.
+ */
+ @AliasFor("query")
+ String value() default "";
+
+ /**
+ * @return Elasticsearch query to be used when executing query. May contain placeholders eg. ?0. Alias for value
+ * @since 5.0
+ */
+ @AliasFor("value")
+ String query() default "";
+
+ /**
+ * Returns whether the query defined should be executed as count projection.
+ *
+ * @return {@literal false} by default.
+ * @since 4.2
+ */
+ boolean count() default false;
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Routing.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Routing.java
new file mode 100644
index 0000000000..ea71e50564
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Routing.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright2020-2021 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;
+
+import org.springframework.data.annotation.Persistent;
+
+/**
+ * Annotation to enable custom routing values for an entity.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.2
+ */
+@Persistent
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE })
+public @interface Routing {
+
+ /**
+ * defines how the routing is determined. Can be either the name of a property or a SpEL expression. See the reference
+ * documentation for examples how to use this annotation.
+ */
+ String value();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java
new file mode 100644
index 0000000000..cc596c54f3
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java
@@ -0,0 +1,20 @@
+package org.springframework.data.elasticsearch.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * Marks a property to be populated with the result of a scripted field retrieved from an Elasticsearch response.
+ * @author Ryan Murfitt
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+public @interface ScriptedField {
+
+ /**
+ * (Optional) The name of the scripted field. Defaults to
+ * the field name.
+ */
+ String name() default "";
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java
new file mode 100644
index 0000000000..f50675d979
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.elasticsearch.annotations;
+
+import org.springframework.data.annotation.QueryAnnotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark a repository method as a search template method. The annotation defines the search template id,
+ * the parameters for the search template are taken from the method's arguments.
+ *
+ * @author P.J. Meisch (pj.meisch@sothawo.com)
+ * @since 5.5
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
+@Documented
+@QueryAnnotation
+public @interface SearchTemplateQuery {
+ /**
+ * The id of the search template. Must not be empt or null.
+ */
+ String id();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java
index c2d746f0ff..926154f1f2 100644
--- a/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2014-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,7 +15,11 @@
*/
package org.springframework.data.elasticsearch.annotations;
-import java.lang.annotation.*;
+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;
import org.springframework.data.annotation.Persistent;
@@ -23,14 +27,84 @@
* Elasticsearch Setting
*
* @author Mohsin Husen
+ * @author Peter-Josef Meisch
*/
@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE})
+@Target({ ElementType.TYPE })
public @interface Setting {
+ /**
+ * Resource path for a settings configuration
+ */
String settingPath() default "";
+ /**
+ * Use server-side settings when creating the index.
+ */
+ boolean useServerConfiguration() default false;
+
+ /**
+ * Number of shards for the index. Used for index creation.
+ * With version 4.0, the default value is changed from 5 to 1 to reflect the change in the default settings of
+ * Elasticsearch which changed to 1 as well in Elasticsearch 7.0.
+ */
+ short shards() default 1;
+
+ /**
+ * Number of replicas for the index. Used for index creation.
+ */
+ short replicas() default 1;
+
+ /**
+ * Refresh interval for the index. Used for index creation.
+ */
+ String refreshInterval() default "1s";
+
+ /**
+ * Index storage type for the index. Used for index creation.
+ */
+ String indexStoreType() default "fs";
+
+ /**
+ * fields to define an index sorting
+ *
+ * @since 4.2
+ */
+ String[] sortFields() default {};
+
+ /**
+ * defines the order for {@link #sortFields()}. If present, it must have the same number of elements
+ *
+ * @since 4.2
+ */
+ SortOrder[] sortOrders() default {};
+
+ /**
+ * defines the mode for {@link #sortFields()}. If present, it must have the same number of elements
+ *
+ * @since 4.2
+ */
+ SortMode[] sortModes() default {};
+
+ /**
+ * defines the missing value for {@link #sortFields()}. If present, it must have the same number of elements
+ *
+ * @since 4.2
+ */
+ SortMissing[] sortMissingValues() default {};
+
+ enum SortOrder {
+ asc, desc
+ }
+
+ enum SortMode {
+ min, max
+ }
+
+ enum SortMissing {
+ _last, _first
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Similarity.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Similarity.java
new file mode 100644
index 0000000000..46cafd91a2
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Similarity.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019-2025 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 Peter-Josef Meisch
+ * @since 4.0
+ */
+public final class Similarity {
+ public final static String Default = "default";
+ public final static String BM25 = "BM25";
+ public final static String classic = "classic";
+ public final static String Boolean = "boolean";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java b/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java
new file mode 100644
index 0000000000..055ecc616f
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation can be placed on repository methods to define the properties that should be requested from
+ * Elasticsearch when the method is run.
+ *
+ * @author Alexander Torres
+ * @author Peter-Josef Meisch
+ * @since 5.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
+@Documented
+public @interface SourceFilters {
+
+ /**
+ * Properties to be requested from Elasticsearch to be included in the response. These can be passed in as literals
+ * like
+ *
+ *
+ * {@code @SourceFilters(includes = {"property1", "property2"})}
+ *
+ *
+ * or as a parameterized value
+ *
+ *
+ * {@code @SourceFilters(includes = "?0")}
+ *
+ *
+ * when the list of properties is passed as a function parameter.
+ */
+ String[] includes() default "";
+
+ /**
+ * Properties to be requested from Elasticsearch to be excluded in the response. These can be passed in as literals
+ * like
+ *
+ *
+ * {@code @SourceFilters(excludes = {"property1", "property2"})}
+ *
+ *
+ * or as a parameterized value
+ *
+ *
+ * {@code @SourceFilters(excludes = "?0")}
+ *
+ *
+ * when the list of properties is passed as a function parameter.
+ */
+ String[] excludes() default "";
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/TermVector.java b/src/main/java/org/springframework/data/elasticsearch/annotations/TermVector.java
new file mode 100644
index 0000000000..25de2cbcad
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/TermVector.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2019-2025 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 Peter-Josef Meisch
+ * @since 4.0
+ */
+public enum TermVector {
+ none, no, yes, with_positions, with_offsets, with_positions_offsets, with_positions_payloads, with_positions_offsets_payloads
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/ValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/annotations/ValueConverter.java
new file mode 100644
index 0000000000..eb848bfed2
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/ValueConverter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021-2025 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.Documented;
+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;
+
+import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter;
+
+/**
+ * Annotation to put on a property of an entity to define a value converter which can convert the property to a type
+ * that Elasticsearch understands and back.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.3
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
+@Documented
+@Inherited
+public @interface ValueConverter {
+
+ /**
+ * Defines the class implementing the {@link PropertyValueConverter} interface. If this is a normal class, it must
+ * provide a default constructor with no arguments. If this is an enum and thus implementing a singleton by enum it
+ * must only have one enum value.
+ *
+ * @return the class to use for conversion
+ */
+ Class extends PropertyValueConverter> value();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/WriteOnlyProperty.java b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteOnlyProperty.java
new file mode 100644
index 0000000000..7704450e26
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteOnlyProperty.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022-2025 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.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark a property that will be written to Elasticsearch, but not set when reading from Elasticsearch.
+ * This is needed for synthesized fields that may be used for search but that are not available in the entity.
+ *
+ * @author Peter-Josef Meisch
+ * @since 5.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.FIELD })
+@Documented
+public @interface WriteOnlyProperty {
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java
new file mode 100644
index 0000000000..86a844cc18
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.elasticsearch.annotations;
+
+import org.springframework.data.mapping.context.MappingContext;
+
+/**
+ * Defines if type hints should be written. Used by {@link Document} annotation.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.3
+ */
+public enum WriteTypeHint {
+
+ /**
+ * Use the global settings from the {@link MappingContext}.
+ */
+ DEFAULT,
+ /**
+ * Always write type hints for the entity.
+ */
+ TRUE,
+ /**
+ * Never write type hints for the entity.
+ */
+ FALSE
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/package-info.java b/src/main/java/org/springframework/data/elasticsearch/annotations/package-info.java
new file mode 100644
index 0000000000..4b8ccdf64e
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/annotations/package-info.java
@@ -0,0 +1,2 @@
+@org.jspecify.annotations.NullMarked
+package org.springframework.data.elasticsearch.annotations;
diff --git a/src/main/java/org/springframework/data/elasticsearch/aot/ElasticsearchAotPredicates.java b/src/main/java/org/springframework/data/elasticsearch/aot/ElasticsearchAotPredicates.java
new file mode 100644
index 0000000000..c3921b8940
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/aot/ElasticsearchAotPredicates.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023-2025 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.aot;
+
+import java.util.function.Predicate;
+
+import org.springframework.data.util.ReactiveWrappers;
+
+/**
+ * @author Peter-Josef Meisch
+ * @since 5.1
+ */
+public class ElasticsearchAotPredicates {
+
+ public static final Predicate IS_REACTIVE_LIBRARY_AVAILABLE = (
+ lib) -> ReactiveWrappers.isAvailable(lib);
+
+ public static boolean isReactorPresent() {
+ return IS_REACTIVE_LIBRARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR);
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/aot/SpringDataElasticsearchRuntimeHints.java b/src/main/java/org/springframework/data/elasticsearch/aot/SpringDataElasticsearchRuntimeHints.java
new file mode 100644
index 0000000000..100b2ae449
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/aot/SpringDataElasticsearchRuntimeHints.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023-2025 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.aot;
+
+import static org.springframework.data.elasticsearch.aot.ElasticsearchAotPredicates.*;
+
+import java.util.Arrays;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.data.elasticsearch.client.elc.EntityAsMap;
+import org.springframework.data.elasticsearch.core.event.AfterConvertCallback;
+import org.springframework.data.elasticsearch.core.event.AfterLoadCallback;
+import org.springframework.data.elasticsearch.core.event.AfterSaveCallback;
+import org.springframework.data.elasticsearch.core.event.BeforeConvertCallback;
+import org.springframework.data.elasticsearch.core.event.ReactiveAfterConvertCallback;
+import org.springframework.data.elasticsearch.core.event.ReactiveAfterLoadCallback;
+import org.springframework.data.elasticsearch.core.event.ReactiveAfterSaveCallback;
+import org.springframework.data.elasticsearch.core.event.ReactiveBeforeConvertCallback;
+
+/**
+ * @author Peter-Josef Meisch
+ * @since 5.1
+ */
+public class SpringDataElasticsearchRuntimeHints implements RuntimeHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
+ hints.reflection().registerTypes( //
+ Arrays.asList( //
+ TypeReference.of(AfterConvertCallback.class), //
+ TypeReference.of(AfterLoadCallback.class), //
+ TypeReference.of(AfterSaveCallback.class), //
+ TypeReference.of(BeforeConvertCallback.class), //
+ TypeReference.of(EntityAsMap.class) //
+ ), //
+ builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INVOKE_PUBLIC_METHODS));
+
+ if (isReactorPresent()) {
+ hints.reflection().registerTypes( //
+ Arrays.asList( //
+ TypeReference.of(ReactiveAfterConvertCallback.class), //
+ TypeReference.of(ReactiveAfterLoadCallback.class), //
+ TypeReference.of(ReactiveAfterSaveCallback.class), //
+ TypeReference.of(ReactiveBeforeConvertCallback.class) //
+ ), //
+ builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INVOKE_PUBLIC_METHODS));
+ }
+
+ // properties needed to log the different versions
+ hints.resources().registerPattern("versions.properties");
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/aot/package-info.java b/src/main/java/org/springframework/data/elasticsearch/aot/package-info.java
new file mode 100644
index 0000000000..56697c1029
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/aot/package-info.java
@@ -0,0 +1,2 @@
+@org.jspecify.annotations.NullMarked
+package org.springframework.data.elasticsearch.aot;
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
new file mode 100644
index 0000000000..f092e2bf6b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2018-2025 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;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.elasticsearch.support.HttpHeaders;
+
+/**
+ * Configuration interface exposing common client configuration properties for Elasticsearch clients.
+ *
+ * @author Mark Paluch
+ * @author Peter-Josef Meisch
+ * @author Huw Ayling-Miller
+ * @author Henrique Amaral
+ * @since 3.2
+ */
+public interface ClientConfiguration {
+
+ /**
+ * Creates a new {@link ClientConfigurationBuilder} instance.
+ *
+ * @return a new {@link ClientConfigurationBuilder} instance.
+ */
+ static ClientConfigurationBuilderWithRequiredEndpoint builder() {
+ return new ClientConfigurationBuilder();
+ }
+
+ /**
+ * Creates a new {@link ClientConfiguration} instance configured to localhost.
+ *
+ *
+ * // "localhost:9200"
+ * ClientConfiguration configuration = ClientConfiguration.localhost();
+ *
+ *
+ * @return a new {@link ClientConfiguration} instance
+ * @see ClientConfigurationBuilder#connectedToLocalhost()
+ */
+ static ClientConfiguration localhost() {
+ return new ClientConfigurationBuilder().connectedToLocalhost().build();
+ }
+
+ /**
+ * Creates a new {@link ClientConfiguration} instance configured to a single host given {@code hostAndPort}. For
+ * example given the endpoint http://localhost:9200
+ *
+ *
+ * ClientConfiguration configuration = ClientConfiguration.create("localhost:9200");
+ *
+ *
+ * @return a new {@link ClientConfigurationBuilder} instance.
+ */
+ static ClientConfiguration create(String hostAndPort) {
+ return new ClientConfigurationBuilder().connectedTo(hostAndPort).build();
+ }
+
+ /**
+ * Creates a new {@link ClientConfiguration} instance configured to a single host given {@link InetSocketAddress}. For
+ * example given the endpoint http://localhost:9200
+ *
+ *
+ * ClientConfiguration configuration = ClientConfiguration
+ * .create(InetSocketAddress.createUnresolved("localhost", 9200));
+ *
+ *
+ * @return a new {@link ClientConfigurationBuilder} instance.
+ */
+ static ClientConfiguration create(InetSocketAddress socketAddress) {
+ return new ClientConfigurationBuilder().connectedTo(socketAddress).build();
+ }
+
+ /**
+ * Returns the configured endpoints.
+ *
+ * @return the configured endpoints.
+ */
+ List getEndpoints();
+
+ /**
+ * Obtain the {@link HttpHeaders} to be used by default.
+ *
+ * @return the {@link HttpHeaders} to be used by default.
+ */
+ HttpHeaders getDefaultHeaders();
+
+ /**
+ * Returns {@literal true} when the client should use SSL.
+ *
+ * @return {@literal true} when the client should use SSL.
+ */
+ boolean useSsl();
+
+ /**
+ * Returns the {@link SSLContext} to use. Can be {@link Optional#empty()} if not configured.
+ *
+ * @return the {@link SSLContext} to use. Can be {@link Optional#empty()} if not configured.
+ */
+ Optional getSslContext();
+
+ /**
+ * @return the optional SHA-256 fingerprint of the self-signed http_ca.crt certificate output by Elasticsearch at
+ * startup time.
+ */
+ Optional getCaFingerprint();
+
+ /**
+ * Returns the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured.
+ *
+ * @return the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured.
+ */
+ Optional getHostNameVerifier();
+
+ /**
+ * Returns the {@link java.time.Duration connect timeout}.
+ *
+ * @see java.net.Socket#connect(SocketAddress, int)
+ */
+ Duration getConnectTimeout();
+
+ /**
+ * Returns the {@link java.time.Duration socket timeout} which is typically applied as SO-timeout/read timeout.
+ *
+ * @see java.net.Socket#setSoTimeout(int)
+ */
+ Duration getSocketTimeout();
+
+ /**
+ * Returns the path prefix that should be prepended to HTTP(s) requests for Elasticsearch behind a proxy.
+ *
+ * @return the path prefix.
+ * @since 4.0
+ */
+ @Nullable
+ String getPathPrefix();
+
+ /**
+ * returns an optionally set proxy in the form host:port
+ *
+ * @return the optional proxy
+ * @since 4.0
+ */
+ Optional getProxy();
+
+ /**
+ * @return the client configuration callbacks
+ * @since 4.3
+ */
+ List> getClientConfigurers();
+
+ /**
+ * @return the supplier for custom headers.
+ */
+ Supplier getHeadersSupplier();
+
+ /**
+ * @author Christoph Strobl
+ */
+ interface ClientConfigurationBuilderWithRequiredEndpoint {
+
+ /**
+ * @param hostAndPort the {@literal host} and {@literal port} formatted as String {@literal host:port}.
+ * @return the {@link MaybeSecureClientConfigurationBuilder}.
+ */
+ default MaybeSecureClientConfigurationBuilder connectedTo(String hostAndPort) {
+ return connectedTo(new String[] { hostAndPort });
+ }
+
+ /**
+ * @param hostAndPorts the list of {@literal host} and {@literal port} combinations formatted as String
+ * {@literal host:port}.
+ * @return the {@link MaybeSecureClientConfigurationBuilder}.
+ */
+ MaybeSecureClientConfigurationBuilder connectedTo(String... hostAndPorts);
+
+ /**
+ * @param endpoint the {@literal host} and {@literal port}.
+ * @return the {@link MaybeSecureClientConfigurationBuilder}.
+ */
+ default MaybeSecureClientConfigurationBuilder connectedTo(InetSocketAddress endpoint) {
+ return connectedTo(new InetSocketAddress[] { endpoint });
+ }
+
+ /**
+ * @param endpoints the list of {@literal host} and {@literal port} combinations.
+ * @return the {@link MaybeSecureClientConfigurationBuilder}.
+ */
+ MaybeSecureClientConfigurationBuilder connectedTo(InetSocketAddress... endpoints);
+
+ /**
+ * Obviously for testing.
+ *
+ * @return the {@link MaybeSecureClientConfigurationBuilder}.
+ */
+ default MaybeSecureClientConfigurationBuilder connectedToLocalhost() {
+ return connectedTo("localhost:9200");
+ }
+ }
+
+ /**
+ * @author Christoph Strobl
+ */
+ interface MaybeSecureClientConfigurationBuilder extends TerminalClientConfigurationBuilder {
+
+ /**
+ * Connect via {@literal https}
+ * NOTE You need to leave out the protocol in
+ * {@link ClientConfigurationBuilderWithRequiredEndpoint#connectedTo(String)}.
+ *
+ * @return the {@link TerminalClientConfigurationBuilder}.
+ */
+ TerminalClientConfigurationBuilder usingSsl();
+
+ /**
+ * Connects using https if flag is true.
+ *
+ * @param flag whether to use https in the connection
+ * @return the {@link TerminalClientConfigurationBuilder}
+ * @since 5.3
+ */
+ TerminalClientConfigurationBuilder usingSsl(boolean flag);
+
+ /**
+ * Connect via {@literal https} using the given {@link SSLContext}.
+ * NOTE You need to leave out the protocol in
+ * {@link ClientConfigurationBuilderWithRequiredEndpoint#connectedTo(String)}.
+ *
+ * @return the {@link TerminalClientConfigurationBuilder}.
+ */
+ TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext);
+
+ /**
+ * Connect via {@literal https} using the givens {@link SSLContext} and HostnameVerifier {@link HostnameVerifier}
+ * .
+ * NOTE You need to leave out the protocol in
+ * {@link ClientConfigurationBuilderWithRequiredEndpoint#connectedTo(String)}.
+ *
+ * @return the {@link TerminalClientConfigurationBuilder}.
+ */
+ TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext, HostnameVerifier hostnameVerifier);
+
+ /**
+ * Connect via https using a SSLContext that is build from the given certificate fingerprint.
+ *
+ * @param caFingerprint the SHA-256 fingerprint of the self-signed http_ca.crt certificate output by Elasticsearch
+ * at startup time.
+ * @return the {@link TerminalClientConfigurationBuilder}.
+ */
+ TerminalClientConfigurationBuilder usingSsl(String caFingerprint);
+ }
+
+ /**
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ */
+ interface TerminalClientConfigurationBuilder {
+
+ /**
+ * @param defaultHeaders must not be {@literal null}.
+ * @return the {@link TerminalClientConfigurationBuilder}
+ */
+ TerminalClientConfigurationBuilder withDefaultHeaders(HttpHeaders defaultHeaders);
+
+ /**
+ * Configure the {@literal milliseconds} for the connect-timeout.
+ *
+ * @param millis the timeout to use.
+ * @return the {@link TerminalClientConfigurationBuilder}
+ * @see #withConnectTimeout(Duration)
+ */
+ default TerminalClientConfigurationBuilder withConnectTimeout(long millis) {
+ return withConnectTimeout(Duration.ofMillis(millis));
+ }
+
+ /**
+ * Configure a {@link java.time.Duration} connect timeout.
+ *
+ * @param timeout the timeout to use. Must not be {@literal null}.
+ * @return the {@link TerminalClientConfigurationBuilder}
+ * @see java.net.Socket#connect(SocketAddress, int)
+ */
+ TerminalClientConfigurationBuilder withConnectTimeout(Duration timeout);
+
+ /**
+ * Configure the {@literal milliseconds} for the socket timeout.
+ *
+ * @param millis the timeout to use.
+ * @return the {@link TerminalClientConfigurationBuilder}
+ * @see #withSocketTimeout(Duration)
+ */
+ default TerminalClientConfigurationBuilder withSocketTimeout(long millis) {
+ return withSocketTimeout(Duration.ofMillis(millis));
+ }
+
+ /**
+ * Configure a {@link java.time.Duration socket timeout} which is typically applied as SO-timeout/read timeout.
+ *
+ * @param timeout the timeout to use. Must not be {@literal null}.
+ * @return the {@link TerminalClientConfigurationBuilder}
+ * @see java.net.Socket#setSoTimeout(int)
+ */
+ TerminalClientConfigurationBuilder withSocketTimeout(Duration timeout);
+
+ /**
+ * Configure the username and password to be sent as a Basic Authentication header
+ *
+ * @param username the username. Must not be {@literal null}.
+ * @param password the password. Must not be {@literal null}.
+ * @return the {@link TerminalClientConfigurationBuilder}
+ */
+ TerminalClientConfigurationBuilder withBasicAuth(String username, String password);
+
+ /**
+ * Configure the path prefix that will be prepended to any HTTP(s) requests
+ *
+ * @param pathPrefix the pathPrefix.
+ * @return the {@link TerminalClientConfigurationBuilder}
+ * @since 4.0
+ */
+ TerminalClientConfigurationBuilder withPathPrefix(String pathPrefix);
+
+ /**
+ * @param proxy a proxy formatted as String {@literal host:port}.
+ * @return the {@link TerminalClientConfigurationBuilder}.
+ */
+ TerminalClientConfigurationBuilder withProxy(String proxy);
+
+ /**
+ * Register a {@link ClientConfigurationCallback} to configure the client.
+ *
+ * @param clientConfigurer configuration callback, must not be {@literal null}.
+ * @return the {@link TerminalClientConfigurationBuilder}.
+ * @since 4.3
+ */
+ TerminalClientConfigurationBuilder withClientConfigurer(ClientConfigurationCallback> clientConfigurer);
+
+ /**
+ * set a supplier for custom headers. This is invoked for every HTTP request to Elasticsearch to retrieve headers
+ * that should be sent with the request. A common use case is passing in authentication headers that may change.
+ *
+ * Note: When used in a reactive environment, the calling of {@link Supplier#get()} function must not do any
+ * blocking operations. It may return {@literal null}.
+ *
+ * @param headers supplier function for headers, must not be {@literal null}
+ * @return the {@link TerminalClientConfigurationBuilder}.
+ * @since 4.0
+ */
+ TerminalClientConfigurationBuilder withHeaders(Supplier headers);
+
+ /**
+ * Build the {@link ClientConfiguration} object.
+ *
+ * @return the {@link ClientConfiguration} object.
+ */
+ ClientConfiguration build();
+ }
+
+ /**
+ * Callback to be executed to configure a client.
+ *
+ * @param the type of the client configuration class.
+ * @since 4.3
+ */
+ @FunctionalInterface
+ interface ClientConfigurationCallback {
+ T configure(T clientConfigurer);
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java
new file mode 100644
index 0000000000..71af992127
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2018-2025 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;
+
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithRequiredEndpoint;
+import org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder;
+import org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder;
+import org.springframework.data.elasticsearch.support.HttpHeaders;
+import org.springframework.util.Assert;
+
+/**
+ * Default builder implementation for {@link ClientConfiguration}.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Peter-Josef Meisch
+ * @author Huw Ayling-Miller
+ * @author Henrique Amaral
+ * @since 3.2
+ */
+class ClientConfigurationBuilder
+ implements ClientConfigurationBuilderWithRequiredEndpoint, MaybeSecureClientConfigurationBuilder {
+
+ private final List hosts = new ArrayList<>();
+ private HttpHeaders headers = new HttpHeaders();
+ private boolean useSsl;
+ @Nullable private SSLContext sslContext;
+ @Nullable private String caFingerprint;
+ @Nullable private HostnameVerifier hostnameVerifier;
+ private Duration connectTimeout = Duration.ofSeconds(10);
+ private Duration soTimeout = Duration.ofSeconds(5);
+ @Nullable private String username;
+ @Nullable private String password;
+ @Nullable private String pathPrefix;
+ @Nullable private String proxy;
+ private Supplier headersSupplier = HttpHeaders::new;
+ private final List> clientConfigurers = new ArrayList<>();
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithRequiredEndpoint#connectedTo(java.lang.String[])
+ */
+ @Override
+ public MaybeSecureClientConfigurationBuilder connectedTo(String... hostAndPorts) {
+
+ Assert.notEmpty(hostAndPorts, "At least one host is required");
+
+ this.hosts.addAll(Arrays.stream(hostAndPorts).map(ClientConfigurationBuilder::parse).toList());
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithRequiredEndpoint#connectedTo(java.net.InetSocketAddress[])
+ */
+ @Override
+ public MaybeSecureClientConfigurationBuilder connectedTo(InetSocketAddress... endpoints) {
+
+ Assert.notEmpty(endpoints, "At least one endpoint is required");
+
+ this.hosts.addAll(Arrays.asList(endpoints));
+
+ return this;
+ }
+
+ @Override
+ public MaybeSecureClientConfigurationBuilder withProxy(String proxy) {
+ Assert.hasLength(proxy, "proxy must not be null or empty");
+ this.proxy = proxy;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder#usingSsl()
+ */
+ @Override
+ public TerminalClientConfigurationBuilder usingSsl() {
+
+ this.useSsl = true;
+ return this;
+ }
+
+ @Override
+ public TerminalClientConfigurationBuilder usingSsl(boolean flag) {
+
+ this.useSsl = flag;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder#usingSsl(javax.net.ssl.SSLContext)
+ */
+ @Override
+ public TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext) {
+
+ Assert.notNull(sslContext, "SSL Context must not be null");
+
+ this.useSsl = true;
+ this.sslContext = sslContext;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder#usingSsl(javax.net.ssl.SSLContext, javax.net.ssl.HostnameVerifier)
+ */
+ @Override
+ public TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext, HostnameVerifier hostnameVerifier) {
+
+ Assert.notNull(sslContext, "SSL Context must not be null");
+ Assert.notNull(hostnameVerifier, "Host Name Verifier must not be null");
+
+ this.useSsl = true;
+ this.sslContext = sslContext;
+ this.hostnameVerifier = hostnameVerifier;
+ return this;
+ }
+
+ @Override
+ public TerminalClientConfigurationBuilder usingSsl(String caFingerprint) {
+
+ Assert.notNull(caFingerprint, "caFingerprint must not be null");
+
+ this.useSsl = true;
+ this.caFingerprint = caFingerprint;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder#withDefaultHeaders(org.springframework.http.HttpHeaders)
+ */
+ @Override
+ public TerminalClientConfigurationBuilder withDefaultHeaders(HttpHeaders defaultHeaders) {
+
+ Assert.notNull(defaultHeaders, "Default HTTP headers must not be null");
+
+ this.headers = new HttpHeaders();
+ this.headers.addAll(defaultHeaders);
+
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder#withConnectTimeout(java.time.Duration)
+ */
+ @Override
+ public TerminalClientConfigurationBuilder withConnectTimeout(Duration timeout) {
+
+ Assert.notNull(timeout, "I/O timeout must not be null!");
+
+ this.connectTimeout = timeout;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder#withTimeout(java.time.Duration)
+ */
+ @Override
+ public TerminalClientConfigurationBuilder withSocketTimeout(Duration timeout) {
+
+ Assert.notNull(timeout, "Socket timeout must not be null!");
+
+ this.soTimeout = timeout;
+ return this;
+ }
+
+ @Override
+ public TerminalClientConfigurationBuilder withBasicAuth(String username, String password) {
+
+ Assert.notNull(username, "username must not be null");
+ Assert.notNull(password, "password must not be null");
+
+ this.username = username;
+ this.password = password;
+
+ return this;
+ }
+
+ @Override
+ public TerminalClientConfigurationBuilder withPathPrefix(String pathPrefix) {
+ this.pathPrefix = pathPrefix;
+
+ return this;
+ }
+
+ @Override
+ public TerminalClientConfigurationBuilder withClientConfigurer(
+ ClientConfiguration.ClientConfigurationCallback> clientConfigurer) {
+
+ Assert.notNull(clientConfigurer, "clientConfigurer must not be null");
+
+ this.clientConfigurers.add(clientConfigurer);
+ return this;
+ }
+
+ @Override
+ public TerminalClientConfigurationBuilder withHeaders(Supplier headers) {
+
+ Assert.notNull(headers, "headersSupplier must not be null");
+
+ this.headersSupplier = headers;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithOptionalDefaultHeaders#build()
+ */
+ @Override
+ public ClientConfiguration build() {
+
+ if (username != null && password != null) {
+ headers.setBasicAuth(username, password);
+ }
+
+ if (sslContext != null && caFingerprint != null) {
+ throw new IllegalArgumentException("Either SSLContext or caFingerprint must be set, but not both");
+ }
+
+ return new DefaultClientConfiguration(hosts, headers, useSsl, sslContext, caFingerprint, soTimeout, connectTimeout,
+ pathPrefix, hostnameVerifier, proxy, clientConfigurers, headersSupplier);
+ }
+
+ private static InetSocketAddress parse(String hostAndPort) {
+ return InetSocketAddressParser.parse(hostAndPort, ElasticsearchHost.DEFAULT_PORT);
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java
new file mode 100644
index 0000000000..ea097bbb59
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2018-2025 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;
+
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.elasticsearch.support.HttpHeaders;
+
+/**
+ * Default {@link ClientConfiguration} implementation.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @author Huw Ayling-Miller
+ * @author Peter-Josef Meisch
+ * @since 3.2
+ */
+class DefaultClientConfiguration implements ClientConfiguration {
+
+ private final List hosts;
+ private final HttpHeaders headers;
+ private final boolean useSsl;
+ @Nullable private final SSLContext sslContext;
+ @Nullable private final String caFingerprint;
+ private final Duration soTimeout;
+ private final Duration connectTimeout;
+ @Nullable private final String pathPrefix;
+ @Nullable private final HostnameVerifier hostnameVerifier;
+ @Nullable private final String proxy;
+ private final Supplier headersSupplier;
+ private final List> clientConfigurers;
+
+ DefaultClientConfiguration(List hosts, HttpHeaders headers, boolean useSsl,
+ @Nullable SSLContext sslContext, @Nullable String caFingerprint, Duration soTimeout, Duration connectTimeout,
+ @Nullable String pathPrefix, @Nullable HostnameVerifier hostnameVerifier, @Nullable String proxy,
+ List> clientConfigurers, Supplier headersSupplier) {
+
+ this.hosts = List.copyOf(hosts);
+ this.headers = headers;
+ this.useSsl = useSsl;
+ this.sslContext = sslContext;
+ this.caFingerprint = caFingerprint;
+ this.soTimeout = soTimeout;
+ this.connectTimeout = connectTimeout;
+ this.pathPrefix = pathPrefix;
+ this.hostnameVerifier = hostnameVerifier;
+ this.proxy = proxy;
+ this.clientConfigurers = clientConfigurers;
+ this.headersSupplier = headersSupplier;
+ }
+
+ @Override
+ public List getEndpoints() {
+ return this.hosts;
+ }
+
+ @Override
+ public HttpHeaders getDefaultHeaders() {
+ return this.headers;
+ }
+
+ @Override
+ public boolean useSsl() {
+ return this.useSsl;
+ }
+
+ @Override
+ public Optional getSslContext() {
+ return Optional.ofNullable(this.sslContext);
+ }
+
+ @Override
+ public Optional getCaFingerprint() {
+ return Optional.ofNullable(this.caFingerprint);
+ }
+
+ @Override
+ public Optional getHostNameVerifier() {
+ return Optional.ofNullable(this.hostnameVerifier);
+ }
+
+ @Override
+ public Duration getConnectTimeout() {
+ return this.connectTimeout;
+ }
+
+ @Override
+ public Duration getSocketTimeout() {
+ return this.soTimeout;
+ }
+
+ @Nullable
+ @Override
+ public String getPathPrefix() {
+ return this.pathPrefix;
+ }
+
+ @Override
+ public Optional getProxy() {
+ return Optional.ofNullable(proxy);
+ }
+
+ @Override
+ public List> getClientConfigurers() {
+ return clientConfigurers;
+ }
+
+ @Override
+ public Supplier getHeadersSupplier() {
+ return headersSupplier;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ElasticsearchHost.java b/src/main/java/org/springframework/data/elasticsearch/client/ElasticsearchHost.java
new file mode 100644
index 0000000000..014acb6328
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/ElasticsearchHost.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2018-2025 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;
+
+import java.net.InetSocketAddress;
+import java.time.Instant;
+
+import org.springframework.util.Assert;
+
+/**
+ * Value Object containing information about Elasticsearch cluster nodes.
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class ElasticsearchHost {
+
+ /**
+ * Default HTTP port for Elasticsearch servers.
+ */
+ public static final int DEFAULT_PORT = 9200;
+
+ private final InetSocketAddress endpoint;
+ private final State state;
+ private final Instant timestamp;
+
+ public ElasticsearchHost(InetSocketAddress endpoint, State state) {
+
+ Assert.notNull(endpoint, "Host must not be null");
+ Assert.notNull(state, "State must not be null");
+
+ this.endpoint = endpoint;
+ this.state = state;
+ this.timestamp = Instant.now();
+ }
+
+ /**
+ * @param host must not be {@literal null}.
+ * @return new instance of {@link ElasticsearchHost}.
+ */
+ public static ElasticsearchHost online(InetSocketAddress host) {
+ return new ElasticsearchHost(host, State.ONLINE);
+ }
+
+ /**
+ * @param host must not be {@literal null}.
+ * @return new instance of {@link ElasticsearchHost}.
+ */
+ public static ElasticsearchHost offline(InetSocketAddress host) {
+ return new ElasticsearchHost(host, State.OFFLINE);
+ }
+
+ /**
+ * Parse a {@literal hostAndPort} string into a {@link InetSocketAddress}.
+ *
+ * @param hostAndPort the string containing host and port or IP address and port in the format {@code host:port}.
+ * @return the parsed {@link InetSocketAddress}.
+ */
+ public static InetSocketAddress parse(String hostAndPort) {
+ return InetSocketAddressParser.parse(hostAndPort, DEFAULT_PORT);
+ }
+
+ /**
+ * @return {@literal true} if the last known {@link State} was {@link State#ONLINE}
+ */
+ public boolean isOnline() {
+ return State.ONLINE.equals(state);
+ }
+
+ /**
+ * @return never {@literal null}.
+ */
+ public InetSocketAddress getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * @return the last known {@link State}.
+ */
+ public State getState() {
+ return state;
+ }
+
+ /**
+ * @return the {@link Instant} the information was captured.
+ */
+ public Instant getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public String toString() {
+ return "ElasticsearchHost(" + endpoint + ", " + state.name() + ')';
+ }
+
+ public enum State {
+ ONLINE, OFFLINE, UNKNOWN
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/InetSocketAddressParser.java b/src/main/java/org/springframework/data/elasticsearch/client/InetSocketAddressParser.java
new file mode 100644
index 0000000000..33f71a49ed
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/InetSocketAddressParser.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018-2025 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;
+
+import java.net.InetSocketAddress;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Utility to parse endpoints in {@code host:port} format into {@link java.net.InetSocketAddress}.
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+public class InetSocketAddressParser {
+
+ /**
+ * Parse a host and port string into a {@link InetSocketAddress}.
+ *
+ * @param hostPortString Hostname/IP address and port formatted as {@code host:port} or {@code host}.
+ * @param defaultPort default port to apply if {@code hostPostString} does not contain a port.
+ * @return a {@link InetSocketAddress} that is unresolved to avoid DNS lookups.
+ * @see InetSocketAddress#createUnresolved(String, int)
+ */
+ public static InetSocketAddress parse(String hostPortString, int defaultPort) {
+
+ Assert.notNull(hostPortString, "HostPortString must not be null");
+ String host;
+ String portString = null;
+
+ if (hostPortString.startsWith("[")) {
+ String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString);
+ host = hostAndPort[0];
+ portString = hostAndPort[1];
+ } else {
+ int colonPos = hostPortString.indexOf(':');
+ if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) {
+ // Exactly 1 colon. Split into host:port.
+ host = hostPortString.substring(0, colonPos);
+ portString = hostPortString.substring(colonPos + 1);
+ } else {
+ // 0 or 2+ colons. Bare hostname or IPv6 literal.
+ host = hostPortString;
+ }
+ }
+
+ int port = defaultPort;
+ if (StringUtils.hasText(portString)) {
+ // Try to parse the whole port string as a number.
+ Assert.isTrue(!portString.startsWith("+"), String.format("Cannot parse port number: %s", hostPortString));
+ try {
+ port = Integer.parseInt(portString);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(String.format("Cannot parse port number: %s", hostPortString));
+ }
+
+ Assert.isTrue(isValidPort(port), String.format("Port number out of range: %s", hostPortString));
+ }
+
+ return InetSocketAddress.createUnresolved(host, port);
+ }
+
+ /**
+ * Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails.
+ *
+ * @param hostPortString the full bracketed host-port specification. Post might not be specified.
+ * @return an array with 2 strings: host and port, in that order.
+ * @throws IllegalArgumentException if parsing the bracketed host-port string fails.
+ */
+ private static String[] getHostAndPortFromBracketedHost(String hostPortString) {
+
+ Assert.isTrue(hostPortString.charAt(0) == '[',
+ String.format("Bracketed host-port string must start with a bracket: %s", hostPortString));
+
+ int colonIndex = hostPortString.indexOf(':');
+ int closeBracketIndex = hostPortString.lastIndexOf(']');
+
+ Assert.isTrue(colonIndex > -1 && closeBracketIndex > colonIndex,
+ String.format("Invalid bracketed host/port: %s", hostPortString));
+
+ String host = hostPortString.substring(1, closeBracketIndex);
+ if (closeBracketIndex + 1 == hostPortString.length()) {
+ return new String[] { host, "" };
+ } else {
+
+ Assert.isTrue(hostPortString.charAt(closeBracketIndex + 1) == ':',
+ "Only a colon may follow a close bracket: " + hostPortString);
+ for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) {
+ Assert.isTrue(Character.isDigit(hostPortString.charAt(i)),
+ String.format("Port must be numeric: %s", hostPortString));
+ }
+ return new String[] { host, hostPortString.substring(closeBracketIndex + 2) };
+ }
+ }
+
+ /**
+ * @param port the port number
+ * @return {@literal true} for valid port numbers.
+ */
+ private static boolean isValidPort(int port) {
+ return port >= 0 && port <= 65535;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/NoReachableHostException.java b/src/main/java/org/springframework/data/elasticsearch/client/NoReachableHostException.java
new file mode 100644
index 0000000000..b8a560db63
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/NoReachableHostException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018-2025 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;
+
+import java.util.Set;
+
+/**
+ * {@link RuntimeException} to be emitted / thrown when the cluster is down (aka none of the known nodes is reachable).
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+public class NoReachableHostException extends RuntimeException {
+
+ public NoReachableHostException(Set hosts) {
+ super(createMessage(hosts));
+ }
+
+ public NoReachableHostException(Set hosts, Throwable cause) {
+ super(createMessage(hosts), cause);
+ }
+
+ private static String createMessage(Set hosts) {
+
+ if (hosts.size() == 1) {
+ return String.format("Host '%s' not reachable. Cluster state is offline.", hosts.iterator().next().getEndpoint());
+ }
+
+ return String.format("No active host found in cluster. (%s) of (%s) nodes offline.", hosts.size(), hosts.size());
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/NodeClientFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/client/NodeClientFactoryBean.java
deleted file mode 100644
index 8caa158416..0000000000
--- a/src/main/java/org/springframework/data/elasticsearch/client/NodeClientFactoryBean.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2013 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
- *
- * http://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;
-
-import static org.elasticsearch.node.NodeBuilder.*;
-
-import org.elasticsearch.client.Client;
-import org.elasticsearch.client.node.NodeClient;
-import org.elasticsearch.common.settings.ImmutableSettings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.DisposableBean;
-import org.springframework.beans.factory.FactoryBean;
-import org.springframework.beans.factory.InitializingBean;
-
-/**
- * NodeClientFactoryBean
- *
- * @author Rizwan Idrees
- * @author Mohsin Husen
- */
-
-public class NodeClientFactoryBean implements FactoryBean, InitializingBean, DisposableBean {
-
- private static final Logger logger = LoggerFactory.getLogger(NodeClientFactoryBean.class);
- private boolean local;
- private boolean enableHttp;
- private String clusterName;
- private NodeClient nodeClient;
-
- NodeClientFactoryBean() {
- }
-
- public NodeClientFactoryBean(boolean local) {
- this.local = local;
- }
-
- @Override
- public NodeClient getObject() throws Exception {
- return nodeClient;
- }
-
- @Override
- public Class extends Client> getObjectType() {
- return NodeClient.class;
- }
-
- @Override
- public boolean isSingleton() {
- return true;
- }
-
- @Override
- public void afterPropertiesSet() throws Exception {
- ImmutableSettings.Builder settings = ImmutableSettings.settingsBuilder().put("http.enabled",
- String.valueOf(this.enableHttp));
-
- nodeClient = (NodeClient) nodeBuilder().settings(settings).clusterName(this.clusterName).local(this.local).node()
- .client();
- }
-
- public void setLocal(boolean local) {
- this.local = local;
- }
-
- public void setEnableHttp(boolean enableHttp) {
- this.enableHttp = enableHttp;
- }
-
- public void setClusterName(String clusterName) {
- this.clusterName = clusterName;
- }
-
- @Override
- public void destroy() throws Exception {
- try {
- logger.info("Closing elasticSearch client");
- if (nodeClient != null) {
- nodeClient.close();
- }
- } catch (final Exception e) {
- logger.error("Error closing ElasticSearch client: ", e);
- }
- }
-}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/TransportClientFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/client/TransportClientFactoryBean.java
deleted file mode 100644
index 3e91ba0c2d..0000000000
--- a/src/main/java/org/springframework/data/elasticsearch/client/TransportClientFactoryBean.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright 2013 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
- *
- * http://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;
-
-import static org.apache.commons.lang.StringUtils.*;
-import static org.elasticsearch.common.settings.ImmutableSettings.*;
-
-import java.util.Properties;
-
-import org.elasticsearch.client.transport.TransportClient;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.transport.InetSocketTransportAddress;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.DisposableBean;
-import org.springframework.beans.factory.FactoryBean;
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.util.Assert;
-
-/**
- * TransportClientFactoryBean
- *
- * @author Rizwan Idrees
- * @author Mohsin Husen
- * @author Jakub Vavrik
- * @author Piotr Betkier
- */
-
-public class TransportClientFactoryBean implements FactoryBean, InitializingBean, DisposableBean {
-
- private static final Logger logger = LoggerFactory.getLogger(TransportClientFactoryBean.class);
- private String clusterNodes = "127.0.0.1:9300";
- private String clusterName = "elasticsearch";
- private Boolean clientTransportSniff = true;
- private Boolean clientIgnoreClusterName = Boolean.FALSE;
- private String clientPingTimeout = "5s";
- private String clientNodesSamplerInterval = "5s";
- private TransportClient client;
- private Properties properties;
- static final String COLON = ":";
- static final String COMMA = ",";
-
- @Override
- public void destroy() throws Exception {
- try {
- logger.info("Closing elasticSearch client");
- if (client != null) {
- client.close();
- }
- } catch (final Exception e) {
- logger.error("Error closing ElasticSearch client: ", e);
- }
- }
-
- @Override
- public TransportClient getObject() throws Exception {
- return client;
- }
-
- @Override
- public Class getObjectType() {
- return TransportClient.class;
- }
-
- @Override
- public boolean isSingleton() {
- return false;
- }
-
- @Override
- public void afterPropertiesSet() throws Exception {
- buildClient();
- }
-
- protected void buildClient() throws Exception {
- client = new TransportClient(settings());
- Assert.hasText(clusterNodes, "[Assertion failed] clusterNodes settings missing.");
- for (String clusterNode : split(clusterNodes, COMMA)) {
- String hostName = substringBefore(clusterNode, COLON);
- String port = substringAfter(clusterNode, COLON);
- Assert.hasText(hostName, "[Assertion failed] missing host name in 'clusterNodes'");
- Assert.hasText(port, "[Assertion failed] missing port in 'clusterNodes'");
- logger.info("adding transport node : " + clusterNode);
- client.addTransportAddress(new InetSocketTransportAddress(hostName, Integer.valueOf(port)));
- }
- client.connectedNodes();
- }
-
- private Settings settings() {
- if (properties != null) {
- return settingsBuilder().put(properties).build();
- }
- return settingsBuilder()
- .put("cluster.name", clusterName)
- .put("client.transport.sniff", clientTransportSniff)
- .put("client.transport.ignore_cluster_name", clientIgnoreClusterName)
- .put("client.transport.ping_timeout", clientPingTimeout)
- .put("client.transport.nodes_sampler_interval", clientNodesSamplerInterval)
- .build();
- }
-
- public void setClusterNodes(String clusterNodes) {
- this.clusterNodes = clusterNodes;
- }
-
- public void setClusterName(String clusterName) {
- this.clusterName = clusterName;
- }
-
- public void setClientTransportSniff(Boolean clientTransportSniff) {
- this.clientTransportSniff = clientTransportSniff;
- }
-
- public String getClientNodesSamplerInterval() {
- return clientNodesSamplerInterval;
- }
-
- public void setClientNodesSamplerInterval(String clientNodesSamplerInterval) {
- this.clientNodesSamplerInterval = clientNodesSamplerInterval;
- }
-
- public String getClientPingTimeout() {
- return clientPingTimeout;
- }
-
- public void setClientPingTimeout(String clientPingTimeout) {
- this.clientPingTimeout = clientPingTimeout;
- }
-
- public Boolean getClientIgnoreClusterName() {
- return clientIgnoreClusterName;
- }
-
- public void setClientIgnoreClusterName(Boolean clientIgnoreClusterName) {
- this.clientIgnoreClusterName = clientIgnoreClusterName;
- }
-
- public void setProperties(Properties properties) {
- this.properties = properties;
- }
-}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedBackendOperation.java b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedBackendOperation.java
new file mode 100644
index 0000000000..0264b95c00
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedBackendOperation.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022-2025 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;
+
+/**
+ * Exception to be thrown by a backend implementation on operations that are not supported for that backend.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.4
+ */
+public class UnsupportedBackendOperation extends RuntimeException {
+ public UnsupportedBackendOperation() {}
+
+ public UnsupportedBackendOperation(String message) {
+ super(message);
+ }
+
+ public UnsupportedBackendOperation(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public UnsupportedBackendOperation(Throwable cause) {
+ super(cause);
+ }
+
+ public UnsupportedBackendOperation(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedClientOperationException.java b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedClientOperationException.java
new file mode 100644
index 0000000000..322646bc66
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedClientOperationException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022-2025 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;
+
+/**
+ * @author Peter-Josef Meisch
+ */
+public class UnsupportedClientOperationException extends RuntimeException {
+ public UnsupportedClientOperationException(Class> clientClass, String operation) {
+ super("Client %1$s does not support the operation %2$s".formatted(clientClass, operation));
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/AbstractQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/AbstractQueryProcessor.java
new file mode 100644
index 0000000000..ff0e1bd3a0
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/AbstractQueryProcessor.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024-2025 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 java.util.function.Consumer;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
+import org.springframework.data.elasticsearch.core.query.Query;
+import org.springframework.data.elasticsearch.core.query.StringQuery;
+
+/**
+ * An abstract class that serves as a base for query processors. It provides a common interface and basic functionality
+ * for query processing.
+ *
+ * @author Aouichaoui Youssef
+ * @since 5.3
+ */
+public abstract class AbstractQueryProcessor {
+
+ /**
+ * Convert a spring-data-elasticsearch {@literal query} to an Elasticsearch {@literal query}.
+ *
+ * @param query spring-data-elasticsearch {@literal query}.
+ * @param queryConverter correct mapped field names and the values to the converted values.
+ * @return an Elasticsearch {@literal query}.
+ */
+
+ static co.elastic.clients.elasticsearch._types.query_dsl.@Nullable Query getEsQuery(@Nullable Query query,
+ @Nullable Consumer queryConverter) {
+ if (query == null) {
+ return null;
+ }
+
+ if (queryConverter != null) {
+ queryConverter.accept(query);
+ }
+
+ co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null;
+
+ if (query instanceof CriteriaQuery criteriaQuery) {
+ esQuery = CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria());
+ } else if (query instanceof StringQuery stringQuery) {
+ esQuery = Queries.wrapperQueryAsQuery(stringQuery.getSource());
+ } else if (query instanceof NativeQuery nativeQuery) {
+ if (nativeQuery.getQuery() != null) {
+ esQuery = nativeQuery.getQuery();
+ } else if (nativeQuery.getSpringDataQuery() != null) {
+ esQuery = getEsQuery(nativeQuery.getSpringDataQuery(), queryConverter);
+ }
+ } else {
+ throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
+ }
+
+ return esQuery;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/Aggregation.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/Aggregation.java
new file mode 100644
index 0000000000..23e2b6ae47
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/Aggregation.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022-2025 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.elasticsearch._types.aggregations.Aggregate;
+
+/**
+ * Class to combine an Elasticsearch {@link co.elastic.clients.elasticsearch._types.aggregations.Aggregate} with its
+ * name. Necessary as the Elasticsearch Aggregate does not know its name.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.4
+ */
+public class Aggregation {
+
+ private final String name;
+ private final Aggregate aggregate;
+
+ public Aggregation(String name, Aggregate aggregate) {
+ this.name = name;
+ this.aggregate = aggregate;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Aggregate getAggregate() {
+ return aggregate;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClient.java
new file mode 100644
index 0000000000..3ce661e1fd
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClient.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021-2025 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.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.transport.ElasticsearchTransport;
+
+import java.io.IOException;
+
+import org.elasticsearch.client.RestClient;
+import org.springframework.util.Assert;
+
+/**
+ * Extension of the {@link ElasticsearchClient} class that implements {@link AutoCloseable}. As the underlying
+ * {@link RestClient} must be closed properly this is handled in the {@link #close()} method.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.4
+ */
+public class AutoCloseableElasticsearchClient extends ElasticsearchClient implements AutoCloseable {
+
+ public AutoCloseableElasticsearchClient(ElasticsearchTransport transport) {
+ super(transport);
+ Assert.notNull(transport, "transport must not be null");
+ }
+
+ @Override
+ public void close() throws IOException {
+ // since Elasticsearch 8.16 the ElasticsearchClient implements (through ApiClient) the Closeable interface and
+ // handles closing of the underlying transport. We now just call the base class, but keep this as we
+ // have been implementing AutoCloseable since 4.4 and won't change that to a mere Closeable
+ super.close();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ChildTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ChildTemplate.java
new file mode 100644
index 0000000000..4d3ebf5bd7
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ChildTemplate.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021-2025 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.cluster.ElasticsearchClusterClient;
+import co.elastic.clients.json.JsonpMapper;
+import co.elastic.clients.transport.Transport;
+
+import java.io.IOException;
+
+import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
+import org.springframework.util.Assert;
+
+/**
+ * base class for a template that uses one of the {@link co.elastic.clients.elasticsearch.ElasticsearchClient}'s child
+ * clients like {@link ElasticsearchClusterClient} or
+ * {@link co.elastic.clients.elasticsearch.indices.ElasticsearchIndicesClient}.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.4
+ */
+public abstract class ChildTemplate> {
+
+ protected final CLIENT client;
+ protected final RequestConverter requestConverter;
+ protected final ResponseConverter responseConverter;
+ protected final ElasticsearchExceptionTranslator exceptionTranslator;
+
+ public ChildTemplate(CLIENT client, ElasticsearchConverter elasticsearchConverter) {
+ this.client = client;
+ JsonpMapper jsonpMapper = client._transport().jsonpMapper();
+ requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper);
+ responseConverter = new ResponseConverter(jsonpMapper);
+ exceptionTranslator = new ElasticsearchExceptionTranslator(jsonpMapper);
+ }
+
+ /**
+ * Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on the client.
+ */
+ @FunctionalInterface
+ public interface ClientCallback {
+ RESULT doWithClient(CLIENT client) throws IOException;
+ }
+
+ /**
+ * Execute a callback with the client and provide exception translation.
+ *
+ * @param callback the callback to execute, must not be {@literal null}
+ * @param the type returned from the callback
+ * @return the callback result
+ */
+ public RESULT execute(ClientCallback callback) {
+
+ Assert.notNull(callback, "callback must not be null");
+
+ try {
+ return callback.doWithClient(client);
+ } catch (IOException | RuntimeException e) {
+ throw exceptionTranslator.translateException(e);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ClusterTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ClusterTemplate.java
new file mode 100644
index 0000000000..fcba35fa7d
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ClusterTemplate.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021-2025 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.elasticsearch.cluster.ElasticsearchClusterClient;
+import co.elastic.clients.elasticsearch.cluster.HealthRequest;
+import co.elastic.clients.elasticsearch.cluster.HealthResponse;
+import co.elastic.clients.transport.ElasticsearchTransport;
+
+import org.springframework.data.elasticsearch.core.cluster.ClusterHealth;
+import org.springframework.data.elasticsearch.core.cluster.ClusterOperations;
+import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
+
+/**
+ * Implementation of the {@link ClusterOperations} interface using en {@link ElasticsearchClusterClient}.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.4
+ */
+public class ClusterTemplate extends ChildTemplate
+ implements ClusterOperations {
+
+ public ClusterTemplate(ElasticsearchClusterClient client, ElasticsearchConverter elasticsearchConverter) {
+ super(client, elasticsearchConverter);
+ }
+
+ @Override
+ public ClusterHealth health() {
+
+ HealthRequest healthRequest = requestConverter.clusterHealthRequest();
+ HealthResponse healthResponse = execute(client -> client.health(healthRequest));
+ return responseConverter.clusterHealth(healthResponse);
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaFilterProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaFilterProcessor.java
new file mode 100644
index 0000000000..702d8501b3
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaFilterProcessor.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright 2021-2025 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.elasticsearch._types.GeoDistanceType;
+import co.elastic.clients.elasticsearch._types.GeoShapeRelation;
+import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
+import co.elastic.clients.elasticsearch._types.query_dsl.GeoBoundingBoxQuery;
+import co.elastic.clients.elasticsearch._types.query_dsl.GeoDistanceQuery;
+import co.elastic.clients.elasticsearch._types.query_dsl.GeoShapeQuery;
+import co.elastic.clients.elasticsearch._types.query_dsl.Query;
+import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
+import co.elastic.clients.elasticsearch._types.query_dsl.QueryVariant;
+import co.elastic.clients.json.JsonData;
+import co.elastic.clients.util.ObjectBuilder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.springframework.data.elasticsearch.core.convert.GeoConverters;
+import org.springframework.data.elasticsearch.core.geo.GeoBox;
+import org.springframework.data.elasticsearch.core.geo.GeoJson;
+import org.springframework.data.elasticsearch.core.geo.GeoPoint;
+import org.springframework.data.elasticsearch.core.query.Criteria;
+import org.springframework.data.elasticsearch.utils.geohash.Geohash;
+import org.springframework.data.geo.Box;
+import org.springframework.data.geo.Distance;
+import org.springframework.data.geo.Metrics;
+import org.springframework.data.geo.Point;
+import org.springframework.util.Assert;
+
+/**
+ * Class to convert a {@link org.springframework.data.elasticsearch.core.query.CriteriaQuery} into an Elasticsearch
+ * filter.
+ *
+ * @author Peter-Josef Meisch
+ * @author Junghoon Ban
+ * @since 4.4
+ */
+class CriteriaFilterProcessor {
+ /**
+ * Creates a filter query from the given criteria.
+ *
+ * @param criteria the criteria to process
+ * @return the optional query, empty if the criteria did not contain filter relevant elements
+ */
+ public static Optional createQuery(Criteria criteria) {
+
+ Assert.notNull(criteria, "criteria must not be null");
+
+ List filterQueries = new ArrayList<>();
+
+ for (Criteria chainedCriteria : criteria.getCriteriaChain()) {
+
+ if (chainedCriteria.isOr()) {
+ Collection extends Query> queriesForEntries = queriesForEntries(chainedCriteria);
+
+ if (!queriesForEntries.isEmpty()) {
+ BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
+ queriesForEntries.forEach(boolQueryBuilder::should);
+ filterQueries.add(new Query(boolQueryBuilder.build()));
+ }
+ } else if (chainedCriteria.isNegating()) {
+
+ Assert.notNull(criteria.getField(), "criteria must have a field");
+
+ Collection extends Query> negatingFilters = buildNegatingFilter(criteria.getField().getName(),
+ criteria.getFilterCriteriaEntries());
+ filterQueries.addAll(negatingFilters);
+ } else {
+ filterQueries.addAll(queriesForEntries(chainedCriteria));
+ }
+ }
+
+ if (filterQueries.isEmpty()) {
+ return Optional.empty();
+ } else {
+
+ if (filterQueries.size() == 1) {
+ return Optional.of(filterQueries.get(0));
+ } else {
+ BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
+ filterQueries.forEach(boolQueryBuilder::must);
+ BoolQuery boolQuery = boolQueryBuilder.build();
+ return Optional.of(new Query(boolQuery));
+ }
+ }
+ }
+
+ private static Collection extends Query> buildNegatingFilter(String fieldName,
+ Set filterCriteriaEntries) {
+
+ List negationFilters = new ArrayList<>();
+
+ filterCriteriaEntries.forEach(criteriaEntry -> {
+ Optional query = queryFor(criteriaEntry.getKey(), criteriaEntry.getValue(), fieldName);
+
+ if (query.isPresent()) {
+ BoolQuery negatingFilter = QueryBuilders.bool().mustNot(query.get()).build();
+ negationFilters.add(new Query(negatingFilter));
+ }
+ });
+
+ return negationFilters;
+ }
+
+ private static Collection extends Query> queriesForEntries(Criteria criteria) {
+
+ Assert.notNull(criteria.getField(), "criteria must have a field");
+
+ String fieldName = criteria.getField().getName();
+ Assert.notNull(fieldName, "Unknown field");
+
+ return criteria.getFilterCriteriaEntries().stream()
+ .map(entry -> queryFor(entry.getKey(), entry.getValue(), fieldName)) //
+ .filter(Optional::isPresent) //
+ .map(Optional::get) //
+ .collect(Collectors.toList());
+ }
+
+ private static Optional queryFor(Criteria.OperationKey key, Object value, String fieldName) {
+
+ ObjectBuilder extends QueryVariant> queryBuilder = null;
+
+ switch (key) {
+ case WITHIN -> {
+ Assert.isTrue(value instanceof Object[], "Value of a geo distance filter should be an array of two values.");
+ queryBuilder = withinQuery(fieldName, (Object[]) value);
+ }
+ case BBOX -> {
+ Assert.isTrue(value instanceof Object[],
+ "Value of a boundedBy filter should be an array of one or two values.");
+ queryBuilder = boundingBoxQuery(fieldName, (Object[]) value);
+ }
+ case GEO_INTERSECTS -> {
+ Assert.isTrue(value instanceof GeoJson>, "value of a GEO_INTERSECTS filter must be a GeoJson object");
+ queryBuilder = geoJsonQuery(fieldName, (GeoJson>) value, "intersects");
+ }
+ case GEO_IS_DISJOINT -> {
+ Assert.isTrue(value instanceof GeoJson>, "value of a GEO_IS_DISJOINT filter must be a GeoJson object");
+ queryBuilder = geoJsonQuery(fieldName, (GeoJson>) value, "disjoint");
+ }
+ case GEO_WITHIN -> {
+ Assert.isTrue(value instanceof GeoJson>, "value of a GEO_WITHIN filter must be a GeoJson object");
+ queryBuilder = geoJsonQuery(fieldName, (GeoJson>) value, "within");
+ }
+ case GEO_CONTAINS -> {
+ Assert.isTrue(value instanceof GeoJson>, "value of a GEO_CONTAINS filter must be a GeoJson object");
+ queryBuilder = geoJsonQuery(fieldName, (GeoJson>) value, "contains");
+ }
+ }
+
+ return Optional.ofNullable(queryBuilder != null ? queryBuilder.build()._toQuery() : null);
+ }
+
+ private static ObjectBuilder withinQuery(String fieldName, Object... values) {
+
+ Assert.noNullElements(values, "Geo distance filter takes 2 not null elements array as parameter.");
+ Assert.isTrue(values.length == 2, "Geo distance filter takes a 2-elements array as parameter.");
+ Assert.isTrue(values[0] instanceof GeoPoint || values[0] instanceof String || values[0] instanceof Point,
+ "First element of a geo distance filter must be a GeoPoint, a Point or a text");
+ Assert.isTrue(values[1] instanceof String || values[1] instanceof Distance,
+ "Second element of a geo distance filter must be a text or a Distance");
+
+ String dist = (values[1] instanceof Distance distance) ? extractDistanceString(distance) : (String) values[1];
+
+ return QueryBuilders.geoDistance() //
+ .field(fieldName) //
+ .distance(dist) //
+ .distanceType(GeoDistanceType.Plane) //
+ .location(location -> {
+ if (values[0] instanceof GeoPoint loc) {
+ location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon()));
+ } else if (values[0] instanceof Point point) {
+ GeoPoint loc = GeoPoint.fromPoint(point);
+ location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon()));
+ } else {
+ String loc = (String) values[0];
+ if (loc.contains(",")) {
+ String[] c = loc.split(",");
+ location.latlon(latlon -> latlon.lat(Double.parseDouble(c[0])).lon(Double.parseDouble(c[1])));
+ } else {
+ location.geohash(geohash -> geohash.geohash(loc));
+ }
+ }
+ return location;
+ });
+ }
+
+ private static ObjectBuilder boundingBoxQuery(String fieldName, Object... values) {
+
+ Assert.noNullElements(values, "Geo boundedBy filter takes a not null element array as parameter.");
+
+ GeoBoundingBoxQuery.Builder queryBuilder = QueryBuilders.geoBoundingBox() //
+ .field(fieldName);
+
+ if (values.length == 1) {
+ // GeoEnvelop
+ oneParameterBBox(queryBuilder, values[0]);
+ } else if (values.length == 2) {
+ // 2x GeoPoint
+ // 2x text
+ twoParameterBBox(queryBuilder, values);
+ } else {
+ throw new IllegalArgumentException(
+ "Geo distance filter takes a 1-elements array(GeoBox) or 2-elements array(GeoPoints or Strings(format lat,lon or geohash)).");
+ }
+ return queryBuilder;
+ }
+
+ private static void oneParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, Object value) {
+ Assert.isTrue(value instanceof GeoBox || value instanceof Box,
+ "single-element of boundedBy filter must be type of GeoBox or Box");
+
+ GeoBox geoBBox;
+ if (value instanceof Box box) {
+ geoBBox = GeoBox.fromBox(box);
+ } else {
+ geoBBox = (GeoBox) value;
+ }
+
+ queryBuilder.boundingBox(bb -> bb //
+ .tlbr(tlbr -> tlbr //
+ .topLeft(glb -> glb //
+ .latlon(latlon -> latlon //
+ .lat(geoBBox.getTopLeft().getLat()) //
+ .lon(geoBBox.getTopLeft().getLon()))) //
+ .bottomRight(glb -> glb //
+ .latlon(latlon -> latlon //
+ .lat(geoBBox.getBottomRight().getLat())//
+ .lon(geoBBox.getBottomRight().getLon()// )
+ )))));
+ }
+
+ private static void twoParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, Object... values) {
+
+ Assert.isTrue(allElementsAreOfType(values, GeoPoint.class) || allElementsAreOfType(values, String.class),
+ " both elements of boundedBy filter must be type of GeoPoint or text(format lat,lon or geohash)");
+
+ if (values[0] instanceof GeoPoint topLeft) {
+ GeoPoint bottomRight = (GeoPoint) values[1];
+ queryBuilder.boundingBox(bb -> bb //
+ .tlbr(tlbr -> tlbr //
+ .topLeft(glb -> glb //
+ .latlon(latlon -> latlon //
+ .lat(topLeft.getLat()) //
+ .lon(topLeft.getLon()))) //
+ .bottomRight(glb -> glb //
+ .latlon(latlon -> latlon //
+ .lat(bottomRight.getLat()) //
+ .lon(bottomRight.getLon()))) //
+ ) //
+ );
+ } else {
+ String topLeft = (String) values[0];
+ String bottomRight = (String) values[1];
+ boolean isGeoHash = !topLeft.contains(",");
+ queryBuilder.boundingBox(bb -> bb //
+ .tlbr(tlbr -> tlbr //
+ .topLeft(glb -> {
+ if (isGeoHash) {
+ // although the builder in 8.13.2 supports geohash, the server throws an error, so we convert to a
+ // lat,lon string here
+ glb.text(Geohash.toLatLon(topLeft));
+ // glb.geohash(gh -> gh.geohash(topLeft));
+ } else {
+ glb.text(topLeft);
+ }
+ return glb;
+ }) //
+ .bottomRight(glb -> {
+ if (isGeoHash) {
+ glb.text(Geohash.toLatLon(bottomRight));
+ // glb.geohash(gh -> gh.geohash(bottomRight));
+ } else {
+ glb.text(bottomRight);
+ }
+ return glb;
+ }) //
+ ));
+ }
+ }
+
+ private static boolean allElementsAreOfType(Object[] array, Class> clazz) {
+ for (Object o : array) {
+ if (!clazz.isInstance(o)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static ObjectBuilder extends QueryVariant> geoJsonQuery(String fieldName, GeoJson> geoJson,
+ String relation) {
+ return buildGeoShapeQuery(fieldName, geoJson, relation);
+ }
+
+ private static ObjectBuilder buildGeoShapeQuery(String fieldName, GeoJson> geoJson,
+ String relation) {
+ return QueryBuilders.geoShape().field(fieldName) //
+ .shape(gsf -> gsf //
+ .shape(JsonData.of(GeoConverters.GeoJsonToMapConverter.INSTANCE.convert(geoJson))) //
+ .relation(toRelation(relation))); //
+ }
+
+ private static GeoShapeRelation toRelation(String relation) {
+
+ for (GeoShapeRelation geoShapeRelation : GeoShapeRelation.values()) {
+
+ if (geoShapeRelation.name().equalsIgnoreCase(relation)) {
+ return geoShapeRelation;
+ }
+ }
+ throw new IllegalArgumentException("Unknown geo_shape relation: " + relation);
+ }
+
+ /**
+ * extract the distance string from a {@link org.springframework.data.geo.Distance} object.
+ *
+ * @param distance distance object to extract string from
+ */
+ private static String extractDistanceString(Distance distance) {
+
+ StringBuilder sb = new StringBuilder();
+ sb.append((int) distance.getValue());
+ switch ((Metrics) distance.getMetric()) {
+ case KILOMETERS -> sb.append("km");
+ case MILES -> sb.append("mi");
+ }
+
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryException.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryException.java
new file mode 100644
index 0000000000..cb6cccf973
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021-2025 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 org.springframework.dao.UncategorizedDataAccessException;
+
+/**
+ * @author Peter-Josef Meisch
+ * @since 4.4
+ */
+public class CriteriaQueryException extends UncategorizedDataAccessException {
+ public CriteriaQueryException(String msg) {
+ super(msg, null);
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java
new file mode 100644
index 0000000000..1c9c9ef53a
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2021-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.elasticsearch.client.elc;
+
+import static org.springframework.data.elasticsearch.client.elc.Queries.*;
+import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
+import static org.springframework.util.StringUtils.*;
+
+import co.elastic.clients.elasticsearch._types.FieldValue;
+import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
+import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
+import co.elastic.clients.elasticsearch._types.query_dsl.Query;
+import co.elastic.clients.elasticsearch.core.search.InnerHits;
+import co.elastic.clients.json.JsonData;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.elasticsearch.annotations.FieldType;
+import org.springframework.data.elasticsearch.core.query.Criteria;
+import org.springframework.data.elasticsearch.core.query.Field;
+import org.springframework.data.elasticsearch.core.query.HasChildQuery;
+import org.springframework.data.elasticsearch.core.query.HasParentQuery;
+import org.springframework.data.elasticsearch.core.query.InnerHitsQuery;
+import org.springframework.util.Assert;
+
+/**
+ * Class to convert a {@link org.springframework.data.elasticsearch.core.query.CriteriaQuery} into an Elasticsearch
+ * query.
+ *
+ * @author Peter-Josef Meisch
+ * @author Ezequiel Antúnez Camacho
+ * @since 4.4
+ */
+class CriteriaQueryProcessor extends AbstractQueryProcessor {
+
+ /**
+ * creates a query from the criteria
+ *
+ * @param criteria the {@link Criteria}
+ * @return the optional query, null if the criteria did not contain filter relevant elements
+ */
+ @Nullable
+ public static Query createQuery(Criteria criteria) {
+
+ Assert.notNull(criteria, "criteria must not be null");
+
+ List shouldQueries = new ArrayList<>();
+ List mustNotQueries = new ArrayList<>();
+ List mustQueries = new ArrayList<>();
+
+ Query firstQuery = null;
+ boolean negateFirstQuery = false;
+
+ for (Criteria chainedCriteria : criteria.getCriteriaChain()) {
+ Query queryFragment = queryForEntries(chainedCriteria);
+
+ if (queryFragment != null) {
+
+ if (firstQuery == null) {
+ firstQuery = queryFragment;
+ negateFirstQuery = chainedCriteria.isNegating();
+ continue;
+ }
+
+ if (chainedCriteria.isOr()) {
+ shouldQueries.add(queryFragment);
+ } else if (chainedCriteria.isNegating()) {
+ mustNotQueries.add(queryFragment);
+ } else {
+ mustQueries.add(queryFragment);
+ }
+ }
+ }
+
+ for (Criteria subCriteria : criteria.getSubCriteria()) {
+ Query subQuery = createQuery(subCriteria);
+ if (subQuery != null) {
+ if (criteria.isOr()) {
+ shouldQueries.add(subQuery);
+ } else if (criteria.isNegating()) {
+ mustNotQueries.add(subQuery);
+ } else {
+ mustQueries.add(subQuery);
+ }
+ }
+ }
+
+ if (firstQuery != null) {
+
+ if (!shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) {
+ shouldQueries.add(0, firstQuery);
+ } else {
+
+ if (negateFirstQuery) {
+ mustNotQueries.add(0, firstQuery);
+ } else {
+ mustQueries.add(0, firstQuery);
+ }
+ }
+ }
+
+ var filterQuery = CriteriaFilterProcessor.createQuery(criteria);
+ if (shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) {
+
+ if (filterQuery.isEmpty()) {
+ return null;
+ }
+
+ // we need something to add the filter to
+ mustQueries.add(Query.of(qb -> qb.matchAll(m -> m)));
+ }
+
+ return new Query.Builder().bool(boolQueryBuilder -> {
+
+ if (!shouldQueries.isEmpty()) {
+ boolQueryBuilder.should(shouldQueries);
+ }
+
+ if (!mustNotQueries.isEmpty()) {
+ boolQueryBuilder.mustNot(mustNotQueries);
+ }
+
+ if (!mustQueries.isEmpty()) {
+ boolQueryBuilder.must(mustQueries);
+ }
+
+ filterQuery.ifPresent(boolQueryBuilder::filter);
+
+ return boolQueryBuilder;
+ }).build();
+ }
+
+ @Nullable
+ private static Query queryForEntries(Criteria criteria) {
+
+ Field field = criteria.getField();
+
+ if (field == null || criteria.getQueryCriteriaEntries().isEmpty())
+ return null;
+
+ String fieldName = field.getName();
+ Assert.notNull(fieldName, "Unknown field " + fieldName);
+
+ Iterator it = criteria.getQueryCriteriaEntries().iterator();
+
+ Float boost = Float.isNaN(criteria.getBoost()) ? null : criteria.getBoost();
+ Query.Builder queryBuilder;
+
+ if (criteria.getQueryCriteriaEntries().size() == 1) {
+ queryBuilder = queryFor(it.next(), field, boost);
+ } else {
+ queryBuilder = new Query.Builder();
+ queryBuilder.bool(boolQueryBuilder -> {
+ while (it.hasNext()) {
+ Criteria.CriteriaEntry entry = it.next();
+ boolQueryBuilder.must(queryFor(entry, field, null).build());
+ }
+ boolQueryBuilder.boost(boost);
+ return boolQueryBuilder;
+ });
+
+ }
+
+ if (hasText(field.getPath())) {
+ final Query query = queryBuilder.build();
+ queryBuilder = new Query.Builder();
+ queryBuilder.nested(nqb -> nqb //
+ .path(field.getPath()) //
+ .query(query) //
+ .scoreMode(ChildScoreMode.Avg));
+ }
+
+ if (criteria.isNegating() && criteria.isOr()) {
+ final Query query = queryBuilder.build();
+ queryBuilder = new Query.Builder();
+ queryBuilder.bool(mnqb -> mnqb.mustNot(query));
+ }
+
+ return queryBuilder.build();
+ }
+
+ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field, @Nullable Float boost) {
+
+ String fieldName = field.getName();
+ boolean isKeywordField = FieldType.Keyword == field.getFieldType();
+
+ Criteria.OperationKey key = entry.getKey();
+ Object value = key.hasValue() ? entry.getValue() : null;
+ String searchText = value != null ? escape(value.toString()) : "UNKNOWN_VALUE";
+
+ Query.Builder queryBuilder = new Query.Builder();
+ switch (key) {
+ case EXISTS:
+ queryBuilder //
+ .exists(eb -> eb //
+ .field(fieldName) //
+ .boost(boost));
+ break;
+ case EMPTY:
+ queryBuilder //
+ .bool(bb -> bb //
+ .must(mb -> mb //
+ .exists(eb -> eb //
+ .field(fieldName) //
+ )) //
+ .mustNot(mnb -> mnb //
+ .wildcard(wb -> wb //
+ .field(fieldName) //
+ .wildcard("*"))) //
+ .boost(boost));
+ break;
+ case NOT_EMPTY:
+ queryBuilder //
+ .wildcard(wb -> wb //
+ .field(fieldName) //
+ .wildcard("*") //
+ .boost(boost));
+ break;
+ case EQUALS:
+ queryBuilder.queryString(queryStringQuery(fieldName, searchText, Operator.And, boost));
+ break;
+ case CONTAINS:
+ queryBuilder.queryString(queryStringQuery(fieldName, '*' + searchText + '*', true, boost));
+ break;
+ case STARTS_WITH:
+ queryBuilder.queryString(queryStringQuery(fieldName, searchText + '*', true, boost));
+ break;
+ case ENDS_WITH:
+ queryBuilder.queryString(queryStringQuery(fieldName, '*' + searchText, true, boost));
+ break;
+ case EXPRESSION:
+ queryBuilder.queryString(queryStringQuery(fieldName, Objects.requireNonNull(value).toString(), boost));
+ break;
+ case LESS:
+ queryBuilder
+ .range(rb -> rb
+ .untyped(ut -> ut
+ .field(fieldName)
+ .lt(JsonData.of(value))
+ .boost(boost)));
+ break;
+ case LESS_EQUAL:
+ queryBuilder
+ .range(rb -> rb
+ .untyped(ut -> ut
+ .field(fieldName)
+ .lte(JsonData.of(value))
+ .boost(boost)));
+ break;
+ case GREATER:
+ queryBuilder
+ .range(rb -> rb
+ .untyped(ut -> ut
+ .field(fieldName)
+ .gt(JsonData.of(value))
+ .boost(boost)));
+ break;
+ case GREATER_EQUAL:
+ 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
+ .untyped(ut -> {
+ ut.field(fieldName);
+ if (ranges[0] != null) {
+ ut.gte(JsonData.of(ranges[0]));
+ }
+
+ if (ranges[1] != null) {
+ ut.lte(JsonData.of(ranges[1]));
+ }
+ ut.boost(boost); //
+ return ut;
+ }));
+
+ break;
+ case FUZZY:
+ queryBuilder //
+ .fuzzy(fb -> fb //
+ .field(fieldName) //
+ .value(FieldValue.of(searchText)) //
+ .boost(boost)); //
+ break;
+ case MATCHES:
+ queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.Or, boost));
+ break;
+ case MATCHES_ALL:
+ queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.And, boost));
+
+ break;
+ case IN:
+ if (value instanceof Iterable> iterable) {
+ if (isKeywordField) {
+ queryBuilder.bool(bb -> bb //
+ .must(mb -> mb //
+ .terms(tb -> tb //
+ .field(fieldName) //
+ .terms(tsb -> tsb //
+ .value(toFieldValueList(iterable))))) //
+ .boost(boost)); //
+ } else {
+ queryBuilder //
+ .queryString(qsb -> qsb //
+ .fields(fieldName) //
+ .query(orQueryString(iterable)) //
+ .boost(boost)); //
+ }
+ } else {
+ throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable");
+ }
+ break;
+ case NOT_IN:
+ if (value instanceof Iterable> iterable) {
+ if (isKeywordField) {
+ queryBuilder.bool(bb -> bb //
+ .mustNot(mnb -> mnb //
+ .terms(tb -> tb //
+ .field(fieldName) //
+ .terms(tsb -> tsb //
+ .value(toFieldValueList(iterable))))) //
+ .boost(boost)); //
+ } else {
+ queryBuilder //
+ .queryString(qsb -> qsb //
+ .fields(fieldName) //
+ .query("NOT(" + orQueryString(iterable) + ')') //
+ .boost(boost)); //
+ }
+ } else {
+ throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable");
+ }
+ break;
+ case REGEXP:
+ queryBuilder //
+ .regexp(rb -> rb //
+ .field(fieldName) //
+ .value(Objects.requireNonNull(value).toString()) //
+ .boost(boost)); //
+ break;
+ case HAS_CHILD:
+ if (value instanceof HasChildQuery query) {
+ queryBuilder.hasChild(hcb -> hcb
+ .type(query.getType())
+ .query(getEsQuery(query.getQuery(), null))
+ .innerHits(getInnerHits(query.getInnerHitsQuery()))
+ .ignoreUnmapped(query.getIgnoreUnmapped())
+ .minChildren(query.getMinChildren())
+ .maxChildren(query.getMaxChildren())
+ .scoreMode(scoreMode(query.getScoreMode())));
+ } else {
+ throw new CriteriaQueryException("value for " + fieldName + " is not a has_child query");
+ }
+ break;
+ case HAS_PARENT:
+ if (value instanceof HasParentQuery query) {
+ queryBuilder.hasParent(hpb -> hpb
+ .parentType(query.getParentType())
+ .query(getEsQuery(query.getQuery(), null))
+ .innerHits(getInnerHits(query.getInnerHitsQuery()))
+ .ignoreUnmapped(query.getIgnoreUnmapped())
+ .score(query.getScore()));
+ } else {
+ throw new CriteriaQueryException("value for " + fieldName + " is not a has_parent query");
+ }
+ break;
+ default:
+ throw new CriteriaQueryException("Could not build query for " + entry);
+ }
+
+ return queryBuilder;
+ }
+
+ private static List toFieldValueList(Iterable> iterable) {
+ List