Spring boot with ES Java API Client to Build and Execute Queries in Elasticsearch.
Prerequisites:
Knowledge in Java, Spring boot, Elasticsearch, Kibana.
Concept:
The purpose of this blog is to present an idea for connecting, constructing queries, and querying Elasticsearch through Java applications.
What is Elasticsearch?
Elasticsearch is a distributed, free and open search and analytics engine for all types of data, including textual, numerical, geospatial, structured, and unstructured. It is the central component of the Elastic Stack, a collection of free and open tools for data ingestion, enrichment, storage, analysis, and visualization, and is known for its simple REST APIs, distributed nature, speed, and scalability.
Technologies used:
- Elasticsearch 8.3.3
- Spring boot 2.7.2
- Java 1.8
- Elasticsearch Java API client 7.17.5
- Maven
Tools used:
- Kibana 8.3.3
- Postman
Note: The blog focuses only on a part of the CRUD operation. Click here for the complete source code with CRUD operations.
Project Structure:
Step 1: Create a Spring boot application using Spring Initalizr and select the dependencies as shown in the snapshot below.
Step 2: Add the additional dependencies given in the pom.xml file below.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.poc.es</groupId> <artifactId>elasticsearch-springboot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>elasticsearch-springboot</name> <description>Demo project for integrating elasticsearch with springboot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> <version>7.17.5</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version> </dependency> <dependency> <groupId>jakarta.json</groupId> <artifactId>jakarta.json-api</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> <version>${project.parent.version}</version> </plugin> </plugins> </build> </project>
Step 3: Configuring Elasticsearch in Spring boot application.
application.yml
elastic: index: employees es: hostname: localhost port: 9200 username: admin password: password
ESRestClient.java
package com.poc.es.elasticsearchspringboot.config; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.rest_client.RestClientTransport; import lombok.Getter; import lombok.Setter; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties("es") @Getter @Setter public class ESRestClient { private String hostName; private int port; private String username; private String password; @Bean public ElasticsearchClient getElasticSearchClient() { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); RestClientBuilder builder = RestClient.builder(new HttpHost(hostName, port)) .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); // Create the low-level client RestClient restClient = builder.build(); // Create the transport with a Jackson mapper ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); // And create the API client return new ElasticsearchClient(transport); } }
Step 4: Create Rest controller ESRestController.java
@Autowired private ESService esService; @PostMapping("/index/fetchWithMust") public ResponseEntity<List<Employee>> fetchEmployeesWithMustQuery(@RequestBody Employee employeeSearchRequest) throws IOException { List<Employee> employees = esService.fetchEmployeesWithMustQuery(employeeSearchRequest); return ResponseEntity.ok(employees); } @PostMapping("/index/fetchWithShould") public ResponseEntity<List<Employee>> fetchEmployeesWithShouldQuery(@RequestBody Employee employeeSearchRequest) throws IOException { List<Employee> employees = esService.fetchEmployeesWithShouldQuery(employeeSearchRequest); return ResponseEntity.ok(employees); }
Step 5: Create a model Employee.java.
package com.poc.es.elasticsearchspringboot.model; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Employee { private Long id; private String firstName; private String lastName; private String email; private String gender; private String jobTitle; private String phone; private Integer size; }
Step 6: Create an interface ESService.java and ESServiceImpl.java.
@Service public class ESServiceImpl implements ESService { @Autowired private ESClientConnector esClientConnector; @Override public List<Employee> fetchEmployeesWithMustQuery(Employee employee) throws IOException { return esClientConnector.fetchEmployeesWithMustQuery(employee); } @Override public List<Employee> fetchEmployeesWithShouldQuery(Employee employee) throws IOException { return esClientConnector.fetchEmployeesWithShouldQuery(employee); } }
Step 7: Create a connector class that makes Elasticsearch API calls ESClientConnector.java.
@Value("${elastic.index}") private String index; @Autowired private ElasticsearchClient elasticsearchClient; public List<Employee> fetchEmployeesWithMustQuery(Employee employee) throws IOException { List<Query> queries = prepareQueryList(employee); SearchResponse<Employee> employeeSearchResponse = elasticsearchClient.search(req-> req.index(index) .size(employee.getSize()) .query(query-> query.bool(bool-> bool.must(queries))), Employee.class); return employeeSearchResponse.hits().hits().stream() .map(Hit::source).collect(Collectors.toList()); } public List<Employee> fetchEmployeesWithShouldQuery(Employee employee) throws IOException { List<Query> queries = prepareQueryList(employee); SearchResponse<Employee> employeeSearchResponse = elasticsearchClient.search(req-> req.index(index) .size(employee.getSize()) .query(query-> query.bool(bool-> bool.should(queries))), Employee.class); return employeeSearchResponse.hits().hits().stream() .map(Hit::source).collect(Collectors.toList()); } private List<Query> prepareQueryList(Employee employee) { Map<String, String> conditionMap = new HashMap<>(); conditionMap.put("firstName.keyword", employee.getFirstName()); conditionMap.put("lastName.keyword", employee.getLastName()); conditionMap.put("gender.keyword", employee.getGender()); conditionMap.put("jobTitle.keyword", employee.getJobTitle()); conditionMap.put("phone.keyword", employee.getPhone()); conditionMap.put("email.keyword", employee.getEmail()); return conditionMap.entrySet() .stream() .filter(entry->!ObjectUtils.isEmpty(entry.getValue())) .map(entry->QueryBuilderUtils.termQuery(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); }
Step 8: Create Util interface to build ES queries QueryBuilderUtils.java
package com.poc.es.elasticsearchspringboot.utils; import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch._types.query_dsl.QueryVariant; import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery; public interface QueryBuilderUtils { public static Query termQuery(String field, String value) { QueryVariant queryVariant = new TermQuery.Builder() .caseInsensitive(true) .field(field).value(value).build(); return new Query(queryVariant); } }
API Calls through Postman:
Fetch data from Elasticsearch using MUST clause:
Logs: Constructed ES query with “MUST” clause in Java
POST /employees/_search?typed_keys=true { "query": { "bool": { "must": [ { "term": { "jobTitle.keyword": { "value": "Senior Developer", "case_insensitive": true } } }, { "term": { "gender.keyword": { "value": "Female", "case_insensitive": true } } } ] } } }
Fetch data from Elasticsearch using SHOULD clause:
Logs: Constructed ES query with “SHOULD” clause in Java
POST /employees/_search?typed_keys=true { "query": { "bool": { "should": [ { "term": { "jobTitle.keyword": { "value": "Senior Developer", "case_insensitive": true } } }, { "term": { "gender.keyword": { "value": "Female", "case_insensitive": true } } } ] } } }
Project Github URL: https://github.com/sundharamurali/elasticsearch-springboot.git
Any reference to write mockito test cases for this method ?
How do we search list of values for one field?
Could you please elaborate your question
How can we add custom Mappings and Settings file