Elasticsearch Example using Spring Boot Reactive
Introduction
Elasticsearch is a document-based search engine that is fast and widely used. We can use the Spring boot reactive data libraries to interact with the Elasticsearch server while developing a reactive spring application.
In this article, we will learn how to interact with the Elasticsearch server from a reactive spring boot application.
Technologies used in the article:
- Spring boot: 2.7.3
- Elasticsearch: 7.17.4
- Java: 17
Create a spring boot application
Create a spring boot application with the required dependencies. Add the dependency spring-boot-starter-data-elasticsearch that we can leverage to interact with the Elasticsearch server.
Since we are creating a reactive style application, we will use the spring-boot-starter-webflux starter dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Elasticsearch configuration
Next step is to create a configuration class, that extends AbstractReactiveElasticsearchConfiguration class’s reactiveElasticsearchClient() method. Here, we can specify the Elasticsearch connection string and other properties.
This ReactiveElasticsearchClient instance is used by the spring data repositories while performing the CRUD operations.
@Configuration
public class ReactiveRestClientConfig extends AbstractReactiveElasticsearchConfiguration {
@Override
public ReactiveElasticsearchClient reactiveElasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
return ReactiveRestClients.create(clientConfiguration);
}
}
Creating Elasticsearch documents
We will create a document class that stores student details like name, address, subjects, etc.
First, we will create a Student class with details like enrolled subjects and address details, etc.
@Document(indexName = "student-details")
@Data
public class Student {
@Id
private String id;
@Field(type = FieldType.Text)
private String firstName;
private String lastName;
private int age;
@Field(type = FieldType.Date, format = DateFormat.date)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate joinDate;
private Address address;
@JsonInclude(JsonInclude.Include.NON_NULL)
private List<Subject> subjects;
}
- We can specify the Elasticsearch document by annotating the class with the @Document annotation. We can also mention the index name of the Elasticsearch document.
- Every Elasticsearch document needs a unique id field that is specified by annotating it with the @Id annotation.
- Elasticsearch supports many document field types, and we can explicitly specify the field type with the help of @Field annotation.
Let’s also create an Address class that holds the student’s address details.
@Data
public class Address {
private String street;
private Integer doorNo;
}
Also, create a Subject class as shown below.
@Data
@RequiredArgsConstructor
public class Subject {
private String name;
}
Add the below configuration property into the spring boot application’s application.yml file.
By adding this, Jackson library generates Snake case JSON fields while converting the java classes to JSON.
spring:
jackson:
property-naming-strategy: SNAKE_CASE
Adding spring data repository
Create a StudentRepository interface that extends the ReactiveElasticsearchRepository interface. We can use this repository instance to perform CRUD operations on Elasticsearch documents.
We can also add custom finder methods, as shown below.
public interface StudentRepository extends ReactiveElasticsearchRepository<Student, String> {
Flux<Student> findByFirstName(String firstName);
}
Adding CRUD service implementation
Now let’s create a service layer to add CRUD functionality.
Create a StudentService interface and add the below method signatures.
public interface StudentService {
Mono<Student> createStudent(Student student);
Mono<Student> updateStudent(String id, Student student);
Mono<String> deleteStudent(String id);
Flux<Student> getStudentByFirstName(String firstName);
Flux<Student> getAllStudents();
}
Create an implementation class and implement the methods defined in the service interface.
We use the Elasticsearch repository to interact with the server and CRUD operations.
@RequiredArgsConstructor
@Slf4j
@Service
public class StudentServiceImpl implements StudentService {
private final StudentRepository studentRepository;
@Override
public Mono<Student> createStudent(Student student) {
return studentRepository.save(student);
}
@Override
public Mono<Student> updateStudent(String id, Student student) {
return studentRepository.findById(id).flatMap(std -> {
log.info("std-{}", std);
std.setFirstName(student.getFirstName());
std.setLastName(student.getLastName());
std.setJoinDate(student.getJoinDate());
std.setSubjects(student.getSubjects());
std.setAge(student.getAge());
std.setAddress(student.getAddress());
return studentRepository.save(std);
})
.doOnError(e -> log.error(String.valueOf(e)));
}
@Override
public Mono<String> deleteStudent(String id) {
return studentRepository.deleteById(id)
.thenReturn("Student deleted successfully!");
}
@Override
public Flux<Student> getStudentByFirstName(String firstName) {
return studentRepository.findByFirstName(firstName);
}
@Override
public Flux<Student> getAllStudents() {
return studentRepository.findAll();
}
}
Adding Controller class
Finally, create the REST endpoints to expose the CRUD APIs, as shown below.
@RequiredArgsConstructor
@RestController
public class StudentController {
private final StudentService studentService;
@PostMapping("/students")
public Mono<Student> createStudent(@RequestBody Student student){
return studentService.createStudent(student);
}
@PutMapping("/students/{id}")
public Mono<Student> updateStudent(@RequestBody Student student, @PathVariable("id") String id) {
return studentService.updateStudent(id, student);
}
@DeleteMapping("/students/{id}")
public Mono<String> deleteStudent(@PathVariable("id") String id){
return studentService.deleteStudent(id);
}
@GetMapping("/students/{first-name}")
public Flux<Student> getStudentByFirstname(@PathVariable("first-name") String firstName) {
return studentService.getStudentByFirstName(firstName);
}
@GetMapping("/students")
public Flux<Student> getAllStudents() {
return studentService.getAllStudents();
}
}
Testing the CRUD APIs
We will use the Postman tool to test our CRUD endpoints. By default, the application starts with port number 8080.
Create API
Invoke the POST API /students with the required student request JSON payload, as shown below.
Update API
To update the saved student document, we can use the PUT API: /students/{id}.
Here, the id is the unique student id generated during the document creation.
Get API
We can fetch all the saved student documents by invoking the GET API: /students.
Delete API
To delete a particular student document, we can use the DELETE API: /students/{id}
Conclusion
In this post, we learned how easy it is to create a Spring boot reactive application and perform CRUD operations with Elasticsearch documents.
We also learned how the spring boot framework provides the necessary support for interacting with Elasticsearch by providing the Elasticsearch spring data starter library.