Providing near real-time search and analytics for all types of data is a distributed search and analytics engine called Elasticsearch. It is at the heart of the Elastic stack.
When developing modern software, features like fast searching and logging of information are important for a good user experience. It is important to ensure that the search service in your application does not stress the main database by using a search engine like Elasticsearch.
In this tutorial, I am going to show you how to create indexes and perform search queries using spring-boot and Elasticsearch.
Prerequisites
This guide assumes that you have the following:
- Elasticsearch 8.6 setup. For detailed instructions, setup Elasticsearch
- Java 8 or above installed
What we will learn
- Creating a spring-boot project
- Connecting to an elastic search instance
- Creating an index and adding data to the index
- Searching data
Creating a Spring-boot Project
Download a fresh spring-boot project using Spring Initializer. I will be using maven for demonstration in this guide. Include the following dependencies in your project
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.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-starter-data-Elasticsearch</artifactId>
</dependency>
Connecting to an Elasticsearch Instance
After downloading the initial project and including those dependencies, the next step is to connect to our running Elasticsearch instance. We will start with setting up our properties file. But before then, there are a few things to note:
- Starting with Elasticsearch 8.0, security is enabled and configured by default. This means that to connect to our cluster, we will need to configure our API Client to use HTTPS with the generated CA certificate to make API requests without any problem.
- When starting the Elasticsearch cluster for the first time, you are provided with the password for authentication and the CA certificate (http_ca.crt). Copy the generated certificate to your resources folder in your spring-boot project.
Include the following configuration in our application.properties file:
elastic_search.certificate_name=http_ca.crt
elastic_search.username=elastic
elastic_search.password=esATLike
The Elasticsearch username is the default username and the password is the one generated for us.
Create a class for our Elasticsearch configuration. This class includes an ElasticsearchClient bean which allows us to make API calls to our cluster. For this to be successful, we need our credentials and an SSLContext object that will establish a secure connection with our cluster.
We need the below code to inject values from our properties file into fields:
@Value("${elastic_search.certificate_name}")
String caCertificateName;
@Value("${elastic_search.username}")
String username;
@Value("${elastic_search.password}")
String password;
Add this method that creates an SSLContext to the configuration class:
private SSLContext getSSLContext() {
try {
// since our http_ca.crt file is saved in our resources folder.
String toCrt = new FileSystemResource("src/main/resources/"+caCertificateName).getFile().getAbsolutePath();
Path pathToCaCertificate = Paths.get(toCrt);
/**
* CertificateFactory class is capable of creating Java Certificate instances from binary certificate encodings like X.509
* Certificate is a document that verifies the identity of a person or device claiming to own the public key
*/
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate trustedCertificate = null;
try (InputStream stream = Files.newInputStream(pathToCaCertificate)) {
trustedCertificate = certificateFactory.generateCertificate(stream);
}
/**
* KeyStore is a database that contains keys and can be written and read from a disk
* KeyStore keys include 1. Private keys 2.Public key + certificate
*/
KeyStore trustKeyStore = KeyStore.getInstance("pkcs12");
trustKeyStore.load(null, null);
trustKeyStore.setCertificateEntry("ca", trustedCertificate);
SSLContextBuilder sslContextBuilder = SSLContexts.custom().loadTrustMaterial(trustKeyStore,null);
return sslContextBuilder.build();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return null;
}
ElasticsearchClient is a new and easier client library that replaced the old High-Level Rest Client that was used in versions before 8.0. Add the following code to the class:
@Bean
public ElasticsearchClient getRestClient() {
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(username,password)
);
SSLContext sslContext = getSSLContext();
RestClientBuilder clientBuilder = RestClient.builder(new HttpHost("localhost",9200,"https"))
.setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder
.setDefaultCredentialsProvider(credentialsProvider)
.setSSLContext(sslContext)
);
RestClient client = clientBuilder.build();
ElasticsearchTransport transport = new RestClientTransport(client, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
- Creating an Index And Adding Data To the Index
We are now ready to make API requests to our cluster. The first thing to do is to create an index. An index is a type of data organization mechanism, which will allow a user to partition data in a particular manner. An index is what we would call a database when referring to relational databases. In an index, we store documents with properties that are rows and columns respectively in relational databases. Documents should have a unique id to identify them.
In our project, we will store the profile details of our users in an index. This will allow us to search any user by username or email.
Create a DTO class named ProfileRequest with the following code:
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ProfileRequest {
private String username;
private String email;
private String phone;
}
Those annotations are from Lombok.
And for the response, create another class named EsProfileResponse with the following code:
@AllArgsConstructor
@Data
@Builder
@NoArgsConstructor
public class EsProfileResponse {
private String username;
private String email;
private String phone;
}
Create a class called ProfileIndex. This will have a structure for our index:
@NoArgsConstructor
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProfileIndex {
private String username;
private String email;
public ProfileIndex(String username, String email) {
this.username = username;
this.email = email;
}
}
Create another class called EsProfileService which will be our service class. We first create an instance of the ElasticsearchClient object:
private final ElasticsearchClient elasticsearchClient;
public EsProfileServiceImpl(ElasticsearchClient restClient) {
this.elasticsearchClient = restClient;
}
Add this code that shows how to create an index and store documents in it with their id:
public String addProfileToElasticSearch(ProfileRequest request){
try {
ProfileIndex profileIndex = new ProfileIndex(request.getUsername(), request.getEmail());
IndexRequest.Builder<ProfileIndex> indexBuilder = new IndexRequest.Builder<>();
indexBuilder.index("profile");
indexBuilder.id(profileIndex.getUsername());
indexBuilder.document(profileIndex);
elasticsearchClient.index(indexBuilder.build());
return "Profile added to search engine";
} catch(IOException e) {
e.printStackTrace();
return "Failed to add the profile to search engine";
}
}
- Searching Data
After adding our data to the index, the other task is to search for matches of a query through that data. Add this code to our service class:
public List<EsProfileResponse> searchForProfile(String profileSearchString) {
try {
Query byUsername = MatchQuery.of(
q -> q.field("username").query(profileSearchString)
)._toQuery();
Query byEmail = MatchQuery.of(
q -> q.field("email").query(profileSearchString)
)._toQuery();
SearchResponse<ProfileIndex> response = elasticsearchClient.search(s -> s
.index("profile")
.query(q -> q
.bool(b -> b
.should(byUsername)
.should(byEmail)
)
)
,
ProfileIndex.class
);
List<EsProfileResponse> esProfileResponse = new ArrayList<>();
List<Hit<ProfileIndex>> hits = response.hits().hits();
for (Hit<ProfileIndex> hit : hits) {
ProfileIndex profileIndex = hit.source();
esProfileResponse.add(
new EsProfileResponse(
profileIndex.getUsername(),
profileIndex.getEmail(),
"000"
)
);
}
return esProfileResponse;
} catch(IOException e) {
return null;
}
}
Conclusion
In this tutorial, we have been able to connect to an Elasticsearch instance, create an index with documents, and search through those documents. Your main task now is to create a controller class and make API requests to store documents in the index and search through with a given query.
To learn more about Elasticsearch, you can visit the official guide.