diff --git a/README.md b/README.md
index 26bfe68..2af8112 100644
--- a/README.md
+++ b/README.md
@@ -33,13 +33,18 @@ The Spring Data OpenSearch follows the release model of the Spring Data Elastics
### OpenSearch 2.x / 1.x client libraries
-At the moment, Spring Data OpenSearch provides the possibility to use the `RestHighLevelCLient` to connect to OpenSearch clusters.
+
+At the moment, Spring Data OpenSearch provides the possibility to use either `RestHighLevelCLient` or [OpenSearchClient](https://github.com/opensearch-project/opensearch-java) to connect to OpenSearch clusters.
+
+#### Using `RestHighLevelCLient` (default)
+
+By default, the `RestHighLevelCLient` is configured as the means to communicate with OpenSearch clusters.
```xml
org.opensearch.client
spring-data-opensearch
- 1.3.0
+ 1.4.0
```
@@ -49,7 +54,7 @@ To use Spring Boot 3.x auto configuration support:
org.opensearch.client
spring-data-opensearch-starter
- 1.3.0
+ 1.4.0
```
@@ -59,11 +64,80 @@ To use Spring Boot 3.x auto configuration support for testing:
org.opensearch.client
spring-data-opensearch-test-autoconfigure
- 1.3.0
+ 1.4.0
test
```
+#### Using `OpenSearchClient` (preferred)
+
+To switch over to `OpenSearchClient`, the `opensearch-rest-high-level-client` dependency has to be replaced in favor of `opensearch-java`.
+
+```xml
+
+ org.opensearch.client
+ spring-data-opensearch
+ 1.4.0
+
+
+ org.opensearch.client
+ opensearch-rest-high-level-client
+
+
+
+
+
+ org.opensearch.client
+ opensearch-java
+ 2.10.1
+
+```
+
+To use Spring Boot 3.x auto configuration support:
+
+```xml
+
+ org.opensearch.client
+ spring-data-opensearch-starter
+ 1.4.0
+
+
+ org.opensearch.client
+ opensearch-rest-high-level-client
+
+
+
+
+
+ org.opensearch.client
+ opensearch-java
+ 2.10.1
+
+```
+
+To use Spring Boot 3.x auto configuration support for testing:
+
+```xml
+
+ org.opensearch.client
+ spring-data-opensearch-test-autoconfigure
+ 1.4.0
+ test
+
+
+ org.opensearch.client
+ opensearch-rest-high-level-client
+
+
+
+
+
+ org.opensearch.client
+ opensearch-java
+ 2.10.1
+
+```
+
## Getting Started
Here is a quick teaser of an application using Spring Data Repositories in Java:
@@ -224,7 +298,7 @@ Add the Apache Maven dependency:
org.opensearch.client
spring-data-opensearch
- 1.2.1
+ 1.4.0
```
@@ -251,7 +325,7 @@ Add the Gradle dependency:
```groovy
dependencies {
...
- implementation "org.opensearch.client:spring-data-opensearch:1.2.1"
+ implementation "org.opensearch.client:spring-data-opensearch:1.4.0"
...
}
```
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5649771..a9c4a5f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -31,6 +31,7 @@ dependencyResolutionManagement {
create("opensearchLibs") {
version("opensearch", "2.14.0")
+ library("java-client", "org.opensearch.client:opensearch-java:2.10.3")
library("client", "org.opensearch.client", "opensearch-rest-client").versionRef("opensearch")
library("high-level-client", "org.opensearch.client", "opensearch-rest-high-level-client").versionRef("opensearch")
library("sniffer", "org.opensearch.client", "opensearch-rest-client-sniffer").versionRef("opensearch")
@@ -43,6 +44,10 @@ dependencyResolutionManagement {
library("databind", "com.fasterxml.jackson.core", "jackson-databind").versionRef("jackson")
}
+ create("jakarta") {
+ library("json-bind", "jakarta.json.bind:jakarta.json.bind-api:2.0.0")
+ }
+
create("pluginLibs") {
version("spotless", "6.23.1")
version("editorconfig", "0.0.3")
@@ -78,3 +83,4 @@ include("spring-data-opensearch")
include("spring-data-opensearch-starter")
include("spring-data-opensearch-test-autoconfigure")
include("spring-data-opensearch-examples:spring-boot-gradle")
+include("spring-data-opensearch-examples:spring-boot-java-client-gradle")
diff --git a/spring-data-opensearch-examples/spring-boot-gradle/README.md b/spring-data-opensearch-examples/spring-boot-gradle/README.md
index 8088aac..e1983c3 100644
--- a/spring-data-opensearch-examples/spring-boot-gradle/README.md
+++ b/spring-data-opensearch-examples/spring-boot-gradle/README.md
@@ -6,7 +6,7 @@ This sample project demonstrates the usage of the [Spring Data OpenSearch](https
1. The easiest way to get [OpenSearch](https://opensearch.org) service up and running is by using [Docker](https://www.docker.com/):
```shell
-docker run -p 9200:9200 -e "discovery.type=single-node" opensearchproject/opensearch:2.4.0
+docker run -p 9200:9200 -e "discovery.type=single-node" opensearchproject/opensearch:2.11.1
```
2. Build and run the project using [Gradle](https://gradle.org/):
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/README.md b/spring-data-opensearch-examples/spring-boot-java-client-gradle/README.md
new file mode 100644
index 0000000..4f5416f
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/README.md
@@ -0,0 +1,22 @@
+Spring Data OpenSearch Java Client Spring Boot Example Project
+===
+
+This sample project demonstrates the usage of the [Spring Data OpenSearch](https://github.com/opensearch-project/spring-data-opensearch/) in the typical Spring Boot web application using [opensearch-java](https://github.com/opensearch-project/opensearch-java) client. The application assumes that there is an [OpenSearch](https://opensearch.org) service up and running on the local machine, available at `https://localhost:9200` (protected by basic authentication with default credentials).
+
+1. The easiest way to get [OpenSearch](https://opensearch.org) service up and running is by using [Docker](https://www.docker.com/):
+
+```shell
+docker run -p 9200:9200 -e "discovery.type=single-node" opensearchproject/opensearch:2.11.1
+```
+
+2. Build and run the project using [Gradle](https://gradle.org/):
+
+```shell
+./gradlew :spring-data-opensearch-examples:spring-boot-java-client-gradle:bootRun
+```
+
+3. Exercise the REST endpoint available at: `http://localhost:8080/marketplace`
+
+ - Fetch all products: `curl 'http://localhost:8080/marketplace/search'`
+ - Search products by name: `curl 'http://localhost:8080/marketplace/search?name=pillow'`
+ - Search products by name and price greater than: `curl 'http://localhost:8080/marketplace/search?name=pillow&price=35.0'`
\ No newline at end of file
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/build.gradle.kts b/spring-data-opensearch-examples/spring-boot-java-client-gradle/build.gradle.kts
new file mode 100644
index 0000000..07a1db2
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/build.gradle.kts
@@ -0,0 +1,63 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+plugins {
+ alias(springLibs.plugins.spring.boot)
+ alias(pluginLibs.plugins.spotless)
+ alias(pluginLibs.plugins.editorconfig)
+ id("java-conventions")
+}
+
+buildscript {
+ dependencies {
+ classpath(pluginLibs.editorconfig)
+ classpath(pluginLibs.spotless)
+ }
+}
+
+dependencies {
+ api(project(":spring-data-opensearch")) {
+ exclude("org.opensearch.client", "opensearch-rest-high-level-client")
+ }
+ api(project(":spring-data-opensearch-starter")) {
+ exclude("org.opensearch.client", "opensearch-rest-high-level-client")
+ }
+ implementation(springLibs.boot.web)
+ implementation(jacksonLibs.core)
+ implementation(jacksonLibs.databind)
+ implementation(opensearchLibs.client)
+ implementation(opensearchLibs.java.client)
+ testImplementation(springLibs.test)
+ testImplementation(springLibs.boot.test)
+ testImplementation(springLibs.boot.test.autoconfigure)
+ testImplementation(opensearchLibs.testcontainers)
+ testImplementation(project(":spring-data-opensearch-test-autoconfigure")) {
+ exclude("org.opensearch.client", "opensearch-rest-high-level-client")
+ }
+
+ constraints {
+ implementation("ch.qos.logback:logback-classic") {
+ version {
+ require("1.4.12")
+ }
+ because("Fixes CVE-2023-6378")
+ }
+ }
+}
+
+description = "Spring Data OpenSearch Spring Boot Example Project"
+
+spotless {
+ java {
+ target("src/main/java/**/*.java", "src/test/java/org/opensearch/**/*.java")
+
+ trimTrailingWhitespace()
+ indentWithSpaces()
+ endWithNewline()
+
+ removeUnusedImports()
+ importOrder()
+ }
+}
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/MarketplaceApplication.java b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/MarketplaceApplication.java
new file mode 100644
index 0000000..6c64e9d
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/MarketplaceApplication.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.data.example;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration;
+
+@SpringBootApplication(exclude = ElasticsearchDataAutoConfiguration.class)
+public class MarketplaceApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(MarketplaceConfiguration.class, args);
+ }
+}
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/MarketplaceConfiguration.java b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/MarketplaceConfiguration.java
new file mode 100644
index 0000000..1c0e1f8
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/MarketplaceConfiguration.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.data.example;
+
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
+import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.opensearch.client.RestClientBuilder;
+import org.opensearch.data.example.repository.MarketplaceRepository;
+import org.opensearch.spring.boot.autoconfigure.RestClientBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
+
+@Configuration
+@EnableElasticsearchRepositories(basePackageClasses = MarketplaceRepository.class)
+@ComponentScan(basePackageClasses = MarketplaceConfiguration.class)
+public class MarketplaceConfiguration {
+ /**
+ * Allow to connect to the OpenSearch instance which uses self-signed certificates
+ */
+ @Bean
+ RestClientBuilderCustomizer customizer() {
+ return new RestClientBuilderCustomizer() {
+ @Override
+ public void customize(HttpAsyncClientBuilder builder) {
+ try {
+ builder.setSSLContext(new SSLContextBuilder()
+ .loadTrustMaterial(null, new TrustSelfSignedStrategy())
+ .build());
+ } catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException ex) {
+ throw new RuntimeException("Failed to initialize SSL Context instance", ex);
+ }
+ }
+
+ @Override
+ public void customize(RestClientBuilder builder) {
+ // No additional customizations needed
+ }
+ };
+ }
+}
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/model/Product.java b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/model/Product.java
new file mode 100644
index 0000000..eea797e
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/model/Product.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.data.example.model;
+
+import java.math.BigDecimal;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.annotations.Field;
+import org.springframework.data.elasticsearch.annotations.FieldType;
+
+@Document(indexName = "marketplace")
+public class Product {
+ @Id
+ private String id;
+
+ @Field(type = FieldType.Text, name = "name")
+ private String name;
+
+ @Field(type = FieldType.Double, name = "price")
+ private BigDecimal price;
+
+ @Field(type = FieldType.Integer, name = "quantity")
+ private Integer quantity;
+
+ @Field(type = FieldType.Text, name = "description")
+ private String description;
+
+ @Field(type = FieldType.Keyword, name = "vendor")
+ private String vendor;
+
+ public Product() {}
+
+ public Product(String id, String name, BigDecimal price, Integer quantity, String description, String vendor) {
+ this.id = id;
+ this.name = name;
+ this.price = price;
+ this.quantity = quantity;
+ this.description = description;
+ this.vendor = vendor;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getVendor() {
+ return vendor;
+ }
+
+ public void setVendor(String vendor) {
+ this.vendor = vendor;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+}
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/repository/MarketplaceRepository.java b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/repository/MarketplaceRepository.java
new file mode 100644
index 0000000..da409e0
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/repository/MarketplaceRepository.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.data.example.repository;
+
+import java.math.BigDecimal;
+import java.util.List;
+import org.opensearch.data.example.model.Product;
+import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * See please https://github.com/spring-projects/spring-data-elasticsearch/blob/main/src/main/asciidoc/reference/elasticsearch-repository-queries.adoc
+ */
+@Repository
+public interface MarketplaceRepository extends ElasticsearchRepository {
+ List findByNameLikeAndPriceGreaterThan(String name, BigDecimal price);
+}
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/rest/MarketplaceRestController.java b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/rest/MarketplaceRestController.java
new file mode 100644
index 0000000..8cee80c
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/rest/MarketplaceRestController.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.data.example.rest;
+
+import java.math.BigDecimal;
+import java.util.List;
+import org.opensearch.data.example.model.Product;
+import org.opensearch.data.example.repository.MarketplaceRepository;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/marketplace")
+public class MarketplaceRestController {
+ private final MarketplaceRepository repository;
+
+ public MarketplaceRestController(MarketplaceRepository repository) {
+ this.repository = repository;
+ }
+
+ @GetMapping(value = "/search", produces = MediaType.APPLICATION_JSON_VALUE)
+ public List search(
+ @RequestParam(value = "name", required = false, defaultValue = "") String name,
+ @RequestParam(value = "price", required = false, defaultValue = "0.0") BigDecimal price) {
+ return repository.findByNameLikeAndPriceGreaterThan(name, price);
+ }
+}
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/service/MarketplaceInitializer.java b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/service/MarketplaceInitializer.java
new file mode 100644
index 0000000..8122ee3
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/java/org/opensearch/data/example/service/MarketplaceInitializer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.data.example.service;
+
+import java.math.BigDecimal;
+import org.opensearch.data.example.model.Product;
+import org.opensearch.data.example.repository.MarketplaceRepository;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MarketplaceInitializer implements InitializingBean {
+ private final MarketplaceRepository repository;
+
+ public MarketplaceInitializer(MarketplaceRepository repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ repository.save(new Product(
+ "1",
+ "Utopia Bedding Bed Pillows",
+ new BigDecimal(39.99),
+ 2,
+ "These professionally finished pillows, with high thread counts, provide great comfort against your skin along with added durability "
+ + "that easily resists wear and tear to ensure a finished look for your bedroom.",
+ "Utopia Bedding"));
+
+ repository.save(new Product(
+ "2",
+ "Echo Dot Smart speaker",
+ new BigDecimal(34.99),
+ 10,
+ "Our most popular smart speaker with a fabric design. It is our most compact smart speaker that fits perfectly into small spaces.",
+ "Amazon"));
+ }
+}
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/resources/application.yml b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/resources/application.yml
new file mode 100644
index 0000000..a6ed7ff
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/main/resources/application.yml
@@ -0,0 +1,14 @@
+#
+# Copyright OpenSearch Contributors.
+# SPDX-License-Identifier: Apache-2.0
+#
+
+opensearch:
+ uris: https://localhost:9200
+ username: admin
+ password: admin
+
+spring:
+ jackson:
+ serialization:
+ INDENT_OUTPUT: true
diff --git a/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/test/java/org/opensearch/data/example/repository/MarketplaceRepositoryIntegrationTests.java b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/test/java/org/opensearch/data/example/repository/MarketplaceRepositoryIntegrationTests.java
new file mode 100644
index 0000000..b57a30a
--- /dev/null
+++ b/spring-data-opensearch-examples/spring-boot-java-client-gradle/src/test/java/org/opensearch/data/example/repository/MarketplaceRepositoryIntegrationTests.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.data.example.repository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.opensearch.spring.boot.autoconfigure.test.DataOpenSearchTest;
+import org.opensearch.testcontainers.OpensearchContainer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
+import org.springframework.test.context.ContextConfiguration;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers(disabledWithoutDocker = true)
+@DataOpenSearchTest
+@EnableElasticsearchRepositories(basePackageClasses = MarketplaceRepository.class)
+@ContextConfiguration(initializers = MarketplaceRepositoryIntegrationTests.Initializer.class)
+@Tag("integration-test")
+public class MarketplaceRepositoryIntegrationTests {
+ @Container
+ static final OpensearchContainer> opensearch = new OpensearchContainer<>("opensearchproject/opensearch:2.11.1")
+ .withStartupAttempts(5)
+ .withStartupTimeout(Duration.ofMinutes(2));
+
+ @Test
+ void testMarketplaceRepository(@Autowired MarketplaceRepository repository) {
+ assertThat(repository.findAll()).hasSize(0);
+ }
+
+ static class Initializer implements ApplicationContextInitializer {
+ public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
+ TestPropertyValues.of("opensearch.uris=" + opensearch.getHttpHostAddress())
+ .applyTo(configurableApplicationContext.getEnvironment());
+ }
+ }
+}
diff --git a/spring-data-opensearch-starter/build.gradle.kts b/spring-data-opensearch-starter/build.gradle.kts
index 796411d..53e9392 100644
--- a/spring-data-opensearch-starter/build.gradle.kts
+++ b/spring-data-opensearch-starter/build.gradle.kts
@@ -26,6 +26,8 @@ dependencies {
implementation(opensearchLibs.sniffer) {
exclude("commons-logging", "commons-logging")
}
+ compileOnly(opensearchLibs.java.client)
+ compileOnly(jakarta.json.bind)
testImplementation(springLibs.test) {
exclude("ch.qos.logback", "logback-classic")
}
diff --git a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchClientAutoConfiguration.java b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchClientAutoConfiguration.java
new file mode 100644
index 0000000..9a60259
--- /dev/null
+++ b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchClientAutoConfiguration.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.opensearch.spring.boot.autoconfigure;
+
+import org.opensearch.client.RestClient;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.spring.boot.autoconfigure.OpenSearchClientConfigurations.JsonpMapperConfiguration;
+import org.opensearch.spring.boot.autoconfigure.OpenSearchClientConfigurations.OpenSearchClientConfiguration;
+import org.opensearch.spring.boot.autoconfigure.OpenSearchClientConfigurations.OpenSearchTransportConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for OpenSearch's Java client.
+ */
+@AutoConfiguration(after = { JsonbAutoConfiguration.class, OpenSearchRestClientAutoConfiguration.class })
+@ConditionalOnBean(RestClient.class)
+@ConditionalOnClass(OpenSearchClient.class)
+@Import({ JsonpMapperConfiguration.class, OpenSearchTransportConfiguration.class, OpenSearchClientConfiguration.class })
+public class OpenSearchClientAutoConfiguration {
+}
diff --git a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchClientConfigurations.java b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchClientConfigurations.java
new file mode 100644
index 0000000..5cfcc43
--- /dev/null
+++ b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchClientConfigurations.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright OpenSearch Contributors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.spring.boot.autoconfigure;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.spi.JsonProvider;
+import org.opensearch.client.RestClient;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.jackson.JacksonJsonpMapper;
+import org.opensearch.client.json.jsonb.JsonbJsonpMapper;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.transport.OpenSearchTransport;
+import org.opensearch.client.transport.rest_client.RestClientOptions;
+import org.opensearch.client.transport.rest_client.RestClientTransport;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+class OpenSearchClientConfigurations {
+ @Import({ JacksonJsonpMapperConfiguration.class, JsonbJsonpMapperConfiguration.class })
+ static class JsonpMapperConfiguration {
+ }
+
+ @ConditionalOnMissingBean(JsonpMapper.class)
+ @ConditionalOnClass(ObjectMapper.class)
+ @Configuration(proxyBeanMethods = false)
+ static class JacksonJsonpMapperConfiguration {
+ @Bean
+ JacksonJsonpMapper jacksonJsonpMapper() {
+ return new JacksonJsonpMapper();
+ }
+ }
+
+ @ConditionalOnMissingBean(JsonpMapper.class)
+ @ConditionalOnBean(Jsonb.class)
+ @Configuration(proxyBeanMethods = false)
+ static class JsonbJsonpMapperConfiguration {
+ @Bean
+ JsonbJsonpMapper jsonbJsonpMapper(Jsonb jsonb) {
+ return new JsonbJsonpMapper(JsonProvider.provider(), jsonb);
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnMissingBean(OpenSearchTransport.class)
+ static class OpenSearchTransportConfiguration {
+ @Bean
+ RestClientTransport restClientTransport(RestClient restClient, JsonpMapper jsonMapper,
+ ObjectProvider restClientOptions) {
+ return new RestClientTransport(restClient, jsonMapper, restClientOptions.getIfAvailable());
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnBean(OpenSearchTransport.class)
+ static class OpenSearchClientConfiguration {
+ @Bean
+ @ConditionalOnMissingBean
+ OpenSearchClient opensearchClient(OpenSearchTransport transport) {
+ return new OpenSearchClient(transport);
+ }
+ }
+}
diff --git a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataAutoConfiguration.java b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataAutoConfiguration.java
index bf386a1..3594840 100644
--- a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataAutoConfiguration.java
+++ b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataAutoConfiguration.java
@@ -6,6 +6,8 @@
package org.opensearch.spring.boot.autoconfigure.data;
import org.opensearch.data.client.orhlc.OpenSearchRestTemplate;
+import org.opensearch.data.client.osc.OpenSearchTemplate;
+import org.opensearch.spring.boot.autoconfigure.OpenSearchClientAutoConfiguration;
import org.opensearch.spring.boot.autoconfigure.OpenSearchRestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -18,7 +20,7 @@
* Adaptation of the {@link org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration} to
* the needs of OpenSearch.
*/
-@AutoConfiguration(after = {OpenSearchRestClientAutoConfiguration.class})
-@ConditionalOnClass({OpenSearchRestTemplate.class})
-@Import({OpenSearchDataConfiguration.BaseConfiguration.class})
+@AutoConfiguration(after = {OpenSearchClientAutoConfiguration.class, OpenSearchRestClientAutoConfiguration.class})
+@ConditionalOnClass({OpenSearchRestTemplate.class, OpenSearchTemplate.class})
+@Import({OpenSearchDataConfiguration.BaseConfiguration.class, OpenSearchDataConfiguration.JavaClientConfiguration.class})
public class OpenSearchDataAutoConfiguration {}
diff --git a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataConfiguration.java b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataConfiguration.java
index a5af7a4..332d730 100644
--- a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataConfiguration.java
+++ b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/data/OpenSearchDataConfiguration.java
@@ -6,12 +6,17 @@
package org.opensearch.spring.boot.autoconfigure.data;
import java.util.Collections;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.data.client.osc.OpenSearchTemplate;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.domain.EntityScanner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
@@ -58,4 +63,16 @@ ElasticsearchConverter elasticsearchConverter(
return converter;
}
}
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(OpenSearchClient.class)
+ static class JavaClientConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(value = ElasticsearchOperations.class, name = { "elasticsearchTemplate", "opensearchTemplate" })
+ @ConditionalOnBean(OpenSearchClient.class)
+ OpenSearchTemplate elasticsearchTemplate(OpenSearchClient client, ElasticsearchConverter converter) {
+ return new OpenSearchTemplate(client, converter);
+ }
+ }
}
diff --git a/spring-data-opensearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-data-opensearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 7fc8b31..156c86c 100644
--- a/spring-data-opensearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-data-opensearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,3 +1,4 @@
+org.opensearch.spring.boot.autoconfigure.OpenSearchClientAutoConfiguration
org.opensearch.spring.boot.autoconfigure.OpenSearchRestClientAutoConfiguration
org.opensearch.spring.boot.autoconfigure.OpenSearchRestHighLevelClientAutoConfiguration
org.opensearch.spring.boot.autoconfigure.data.OpenSearchDataAutoConfiguration
diff --git a/spring-data-opensearch-test-autoconfigure/src/main/resources/META-INF/spring/org.opensearch.spring.boot.autoconfigure.test.AutoConfigureDataOpenSearch.imports b/spring-data-opensearch-test-autoconfigure/src/main/resources/META-INF/spring/org.opensearch.spring.boot.autoconfigure.test.AutoConfigureDataOpenSearch.imports
index 7fc8b31..156c86c 100644
--- a/spring-data-opensearch-test-autoconfigure/src/main/resources/META-INF/spring/org.opensearch.spring.boot.autoconfigure.test.AutoConfigureDataOpenSearch.imports
+++ b/spring-data-opensearch-test-autoconfigure/src/main/resources/META-INF/spring/org.opensearch.spring.boot.autoconfigure.test.AutoConfigureDataOpenSearch.imports
@@ -1,3 +1,4 @@
+org.opensearch.spring.boot.autoconfigure.OpenSearchClientAutoConfiguration
org.opensearch.spring.boot.autoconfigure.OpenSearchRestClientAutoConfiguration
org.opensearch.spring.boot.autoconfigure.OpenSearchRestHighLevelClientAutoConfiguration
org.opensearch.spring.boot.autoconfigure.data.OpenSearchDataAutoConfiguration
diff --git a/spring-data-opensearch/build.gradle.kts b/spring-data-opensearch/build.gradle.kts
index d7a1131..c2f47ed 100644
--- a/spring-data-opensearch/build.gradle.kts
+++ b/spring-data-opensearch/build.gradle.kts
@@ -32,7 +32,9 @@ dependencies {
implementation(springLibs.context)
implementation(springLibs.tx)
compileOnly(springLibs.web)
+ compileOnly(opensearchLibs.java.client)
+ testImplementation(opensearchLibs.java.client)
testImplementation("jakarta.enterprise:jakarta.enterprise.cdi-api:3.0.0")
testImplementation("org.slf4j:log4j-over-slf4j:2.0.13")
testImplementation("org.apache.logging.log4j:log4j-core:2.23.1")
diff --git a/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/Aggregation.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/Aggregation.java
new file mode 100644
index 0000000..bc1382a
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/Aggregation.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+import org.opensearch.client.opensearch._types.aggregations.Aggregate;
+
+/**
+ * Class to combine an OpenSearch {@link org.opensearch.client.opensearch._types.aggregations.Aggregate} with its
+ * name. Necessary as the OpenSearch 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/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/AutoCloseableOpenSearchClient.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/AutoCloseableOpenSearchClient.java
new file mode 100644
index 0000000..5c29335
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/AutoCloseableOpenSearchClient.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+import org.opensearch.client.RestClient;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.opensearch.cluster.OpenSearchClusterClient;
+import org.opensearch.client.transport.OpenSearchTransport;
+import org.springframework.util.Assert;
+
+/**
+ * Extension of the {@link OpenSearchClient} 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 AutoCloseableOpenSearchClient extends OpenSearchClient implements AutoCloseable {
+
+ public AutoCloseableOpenSearchClient(OpenSearchTransport transport) {
+ super(transport);
+ Assert.notNull(transport, "transport must not be null");
+ }
+
+ @Override
+ public void close() throws Exception {
+ transport.close();
+ }
+
+ @Override
+ public OpenSearchClusterClient cluster() {
+ return super.cluster();
+ }
+}
diff --git a/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/ChildTemplate.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/ChildTemplate.java
new file mode 100644
index 0000000..58461b4
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/ChildTemplate.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+import java.io.IOException;
+import org.opensearch.client.ApiClient;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.opensearch.cluster.OpenSearchClusterClient;
+import org.opensearch.client.transport.Transport;
+import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
+import org.springframework.util.Assert;
+
+/**
+ * base class for a template that uses one of the {@link org.opensearch.client.opensearch.OpenSearchClient}'s child
+ * clients like {@link OpenSearchClusterClient} or
+ * {@link org.opensearch.client.opensearch.indices.OpenSearchIndicesClient}.
+ *
+ * @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 OpenSearchExceptionTranslator 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 OpenSearchExceptionTranslator(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/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/ClusterTemplate.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/ClusterTemplate.java
new file mode 100644
index 0000000..cc94b97
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/ClusterTemplate.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+import org.opensearch.client.opensearch.cluster.HealthRequest;
+import org.opensearch.client.opensearch.cluster.HealthResponse;
+import org.opensearch.client.opensearch.cluster.OpenSearchClusterClient;
+import org.opensearch.client.transport.OpenSearchTransport;
+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 OpenSearchClusterClient}.
+ *
+ * @author Peter-Josef Meisch
+ * @since 4.4
+ */
+public class ClusterTemplate extends ChildTemplate
+ implements ClusterOperations {
+
+ public ClusterTemplate(OpenSearchClusterClient 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/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaFilterProcessor.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaFilterProcessor.java
new file mode 100644
index 0000000..a14910c
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaFilterProcessor.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2021-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+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.opensearch.client.json.JsonData;
+import org.opensearch.client.opensearch._types.GeoDistanceType;
+import org.opensearch.client.opensearch._types.GeoShapeRelation;
+import org.opensearch.client.opensearch._types.query_dsl.BoolQuery;
+import org.opensearch.client.opensearch._types.query_dsl.GeoBoundingBoxQuery;
+import org.opensearch.client.opensearch._types.query_dsl.GeoDistanceQuery;
+import org.opensearch.client.opensearch._types.query_dsl.GeoShapeQuery;
+import org.opensearch.client.opensearch._types.query_dsl.Query;
+import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders;
+import org.opensearch.client.opensearch._types.query_dsl.QueryVariant;
+import org.opensearch.client.util.ObjectBuilder;
+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.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 OpenSearch
+ * 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()) {
+ BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
+ queriesForEntries(chainedCriteria).forEach(boolQueryBuilder::should);
+ filterQueries.add(new Query(boolQueryBuilder.build()));
+ } else if (chainedCriteria.isNegating()) {
+ 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) {
+ glb.geohash(gh -> gh.geohash(topLeft));
+ } else {
+ glb.text(topLeft);
+ }
+ return glb;
+ }) //
+ .bottomRight(glb -> {
+ if (isGeoHash) {
+ 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/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaQueryException.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaQueryException.java
new file mode 100644
index 0000000..982eecf
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaQueryException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+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/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaQueryProcessor.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaQueryProcessor.java
new file mode 100644
index 0000000..f5d7a60
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/CriteriaQueryProcessor.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2021-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+import static org.opensearch.data.client.osc.Queries.*;
+import static org.springframework.util.StringUtils.*;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.opensearch.client.json.JsonData;
+import org.opensearch.client.opensearch._types.FieldValue;
+import org.opensearch.client.opensearch._types.query_dsl.ChildScoreMode;
+import org.opensearch.client.opensearch._types.query_dsl.Operator;
+import org.opensearch.client.opensearch._types.query_dsl.Query;
+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.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Class to convert a {@link org.springframework.data.elasticsearch.core.query.CriteriaQuery} into an OpenSearch
+ * query.
+ *
+ * @author Peter-Josef Meisch
+ * @author Ezequiel Antúnez Camacho
+ * @since 4.4
+ */
+class CriteriaQueryProcessor {
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ if (shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) {
+ return null;
+ }
+
+ Query query = new Query.Builder().bool(boolQueryBuilder -> {
+
+ if (!shouldQueries.isEmpty()) {
+ boolQueryBuilder.should(shouldQueries);
+ }
+
+ if (!mustNotQueries.isEmpty()) {
+ boolQueryBuilder.mustNot(mustNotQueries);
+ }
+
+ if (!mustQueries.isEmpty()) {
+ boolQueryBuilder.must(mustQueries);
+ }
+
+ return boolQueryBuilder;
+ }).build();
+
+ return query;
+ }
+
+ @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));
+ }
+
+ 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, value.toString(), boost));
+ break;
+ case LESS:
+ queryBuilder //
+ .range(rb -> rb //
+ .field(fieldName) //
+ .lt(JsonData.of(value)) //
+ .boost(boost)); //
+ break;
+ case LESS_EQUAL:
+ queryBuilder //
+ .range(rb -> rb //
+ .field(fieldName) //
+ .lte(JsonData.of(value)) //
+ .boost(boost)); //
+ break;
+ case GREATER:
+ queryBuilder //
+ .range(rb -> rb //
+ .field(fieldName) //
+ .gt(JsonData.of(value)) //
+ .boost(boost)); //
+ break;
+ case GREATER_EQUAL:
+ queryBuilder //
+ .range(rb -> rb //
+ .field(fieldName) //
+ .gte(JsonData.of(value)) //
+ .boost(boost)); //
+ break;
+ case BETWEEN:
+ Object[] ranges = (Object[]) value;
+ queryBuilder //
+ .range(rb -> {
+ rb.field(fieldName);
+ if (ranges[0] != null) {
+ rb.gte(JsonData.of(ranges[0]));
+ }
+
+ if (ranges[1] != null) {
+ rb.lte(JsonData.of(ranges[1]));
+ }
+ rb.boost(boost); //
+ return rb;
+ }); //
+
+ break;
+ case FUZZY:
+ queryBuilder //
+ .fuzzy(fb -> fb //
+ .field(fieldName) //
+ .value(FieldValue.of(searchText)) //
+ .boost(boost)); //
+ break;
+ case MATCHES:
+ queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.Or, boost));
+ break;
+ case MATCHES_ALL:
+ queryBuilder.match(matchQuery(fieldName, 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(value.toString()) //
+ .boost(boost)); //
+ break;
+ default:
+ throw new CriteriaQueryException("Could not build query for " + entry);
+ }
+
+ return queryBuilder;
+ }
+
+ private static List toFieldValueList(Iterable> iterable) {
+ List list = new ArrayList<>();
+ for (Object item : iterable) {
+ list.add(item != null ? FieldValue.of(item.toString()) : null);
+ }
+ return list;
+ }
+
+ private static String orQueryString(Iterable> iterable) {
+ StringBuilder sb = new StringBuilder();
+
+ for (Object item : iterable) {
+
+ if (item != null) {
+
+ if (sb.length() > 0) {
+ sb.append(' ');
+ }
+ sb.append('"');
+ sb.append(escape(item.toString()));
+ sb.append('"');
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns a String where those characters that TextParser expects to be escaped are escaped by a preceding
+ * \
. Copied from Apachae 2 licensed org.apache.lucene.queryparser.flexible.standard.QueryParserUtil
+ * class
+ */
+ public static String escape(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ // These characters are part of the query syntax and must be escaped
+ if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' || c == '^' || c == '['
+ || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~' || c == '*' || c == '?' || c == '|' || c == '&'
+ || c == '/') {
+ sb.append('\\');
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/DocumentAdapters.java b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/DocumentAdapters.java
new file mode 100644
index 0000000..6a917d3
--- /dev/null
+++ b/spring-data-opensearch/src/main/java/org/opensearch/data/client/osc/DocumentAdapters.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2021-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opensearch.data.client.osc;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.opensearch.client.json.JsonData;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.opensearch.core.GetResponse;
+import org.opensearch.client.opensearch.core.MgetResponse;
+import org.opensearch.client.opensearch.core.explain.ExplanationDetail;
+import org.opensearch.client.opensearch.core.get.GetResult;
+import org.opensearch.client.opensearch.core.search.CompletionSuggestOption;
+import org.opensearch.client.opensearch.core.search.Hit;
+import org.opensearch.client.opensearch.core.search.NestedIdentity;
+import org.springframework.data.elasticsearch.core.MultiGetItem;
+import org.springframework.data.elasticsearch.core.document.Document;
+import org.springframework.data.elasticsearch.core.document.Explanation;
+import org.springframework.data.elasticsearch.core.document.NestedMetaData;
+import org.springframework.data.elasticsearch.core.document.SearchDocument;
+import org.springframework.data.elasticsearch.core.document.SearchDocumentAdapter;
+import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Utility class to adapt different Elasticsearch responses to a
+ * {@link org.springframework.data.elasticsearch.core.document.Document}
+ *
+ * @author Peter-Josef Meisch
+ * @author Haibo Liu
+ * @since 4.4
+ */
+final class DocumentAdapters {
+
+ private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class);
+
+ private DocumentAdapters() {}
+
+ /**
+ * Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client.
+ *
+ * @param hit the hit object
+ * @param jsonpMapper to map JsonData objects
+ * @return the created {@link SearchDocument}
+ */
+ @SuppressWarnings("unchecked")
+ public static SearchDocument from(Hit> hit, JsonpMapper jsonpMapper) {
+
+ Assert.notNull(hit, "hit must not be null");
+
+ Map> highlightFields = hit.highlight();
+
+ Map innerHits = new LinkedHashMap<>();
+ hit.innerHits().forEach((name, innerHitsResult) -> {
+ // noinspection ReturnOfNull
+ innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, null,
+ searchDocument -> null, jsonpMapper));
+ });
+
+ NestedMetaData nestedMetaData = from(hit.nested());
+
+ Explanation explanation = from(hit.explanation());
+
+ List matchedQueries = hit.matchedQueries();
+
+ Function