Spring Boot i Elasticsearch

Czas czytania: 5 minut

Elasticsearch to dokumentowa baza danych, która udostępnia bardzo sprawny mechanizm wyszukiwania pełnotekstowego. Jest to potężne narzędzie oparte na Apache Lucece, które umożliwia przeszukiwanie danych w czasie niemalże rzeczywistym. Dodatkowo udostępnia przyjazne API, dzięki któremu można w łatwy i przyjazny użytkownikowi sposób, zarządzać tą bazą danych.

Wielu z nas, programistów, doświadczyło kiedyś problemu z wydajnością dużej bazy danych podczas wyszukiwania w niej informacji. Zwłaszcza jeśli duża część danych w tej bazie była tekstowa. Sam widziałem niejednokrotnie jak wolno może działać aplikacja jeśli nikt wcześniej nie zadbał o poprawną strukturę bazy danych. Brak normalizacji bazy, duplikaty danych oraz nieoptymalne wyszukiwanie sprawiały że po jakimś czasie, gdy już baza napełniła się danymi to aplikacja działała co raz wolniej.

Jak uniknąć chociaż części z tych problemów? Jak sprawić żeby Twoja aplikacja działała płynnie po mimo ogromu danych jakie generuje? Z pomocą przychodzi Elasticsearch.

Uruchomienie Elasticsearch na dockerze

Przejdźmy więc do przykładu, w którym połączymy aplikację opartą o Spring Boot z Elasticsearch. Na początek uruchommy sobie dockera z usługą Elasticsearch. Jeśli nie wiesz jak to zrobić, to zachęcam do zapoznania się z dokumentacją techniczną dostępną pod adresem https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html

Uruchomienie obrazu Elasticsearch sprowadza się do wywołania komendy:

docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.6.2

Po poprawnym odpaleniu dockera oraz wejściu na „http://localhost:9200”, w oknie przeglądarki powinniśmy otrzymać poniższy response:

{
  "name" : "2b22d9ccf50b",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "TT6pCY7mTWmm8KWiRNMyPQ",
  "version" : {
    "number" : "7.6.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "ef48eb35cf30adf4db14086e8aabd07ef6fb113f",
    "build_date" : "2020-03-26T06:34:37.794943Z",
    "build_snapshot" : false,
    "lucene_version" : "8.4.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

Warto w tym miejscu dodać, że Elasticsearch udostępnia REST API na porcie 9200. Dzięki temu rozwiązaniu możemy używać tej bazy także za pomocą użytecznego API, opisanego tutaj: https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html.

Przykład integracji Spring Boot i Elasticsearch

Jeśli docker z Elasticsearch działa poprawnie, to możemy przejść do implementacji aplikacji w Spring Boot. Będzie się ona łączyć bezpośrednio z Elasticsearch za pomocą Spring Data Elasticsearch. Jest to dedykowana zależność do Springa, która w łatwy i prosty sposób pozwala wywoływać requesty do Elasticsearch, za pomocą interfejsów analogicznych do tych z Spring Data.

Poniżej znajduje się plik „pom.xml”, w którym znajduje się dodany starter spring-boot-starter-data-elasticsearch

<?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.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ara</groupId>
    <artifactId>elastic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elastic</name>
    <description>Demo project for Spring Boot and Elasticsearch integration</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Warto zauważyć, że dodałem do projektu zależności projektu Lombok. Pomoże to w późniejszej implementacji aplikacji.

Kolejnym krokiem jest zdefiniowanie połączenia po między naszą aplikacją, a usługą Elasticsearch, którą przed chwilą uruchomiliśmy na dockerze. W tym celu dodamy poniższą klasę „EsConfig.java”. Jest to klasa konfiguracyjna, która dostarcza nam deificeję dwóch beanów; Client i ElasticsearchOperations. Odpowiadają one odpowiednio za możliwość połączenia się z usługą Elasticsearch oraz za zdefiniowanie operacji, które mogą być wykonywane na tej usłudze.

package com.ara.elastic;

import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

import java.net.InetAddress;
import java.net.UnknownHostException;

@Configuration
@EnableElasticsearchRepositories
public class EsConfig {

    @Value("${elasticsearch.cluster.name:docker-cluster}")
    private String clusterName;

    @Value("${elasticsearch.host}")
    private String elasticsearchHost;

    @Value("#{new Integer('${elasticsearch.port}')}")
    private Integer elasticsearchPort;

    @Bean
    public Client client() throws UnknownHostException {
        TransportClient client = new PreBuiltTransportClient(Settings.builder()
                .put("cluster.name", clusterName)
                .build());
        client.addTransportAddress(new TransportAddress(InetAddress.getByName(elasticsearchHost), elasticsearchPort));
        return client;
    }

    @Bean
    public ElasticsearchOperations elasticsearchTemplate() throws UnknownHostException {
        return new ElasticsearchTemplate(client());
    }
}

Do pliku konfiguracyjnego „application.properties”, w celu zdefiniowania danych dostępowych do usługi Elasticsearch, należy dodać poniższe parametry:

elasticsearch.host=127.0.0.1
elasticsearch.port=9300
elasticsearch.cluster.name=docker-cluster

Klasa „Post.java” będzie naszą encją poddawaną persystencji w bazie danych.

package com.ara.elastic.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "blog", type = "post")
public class Post {

    @Id
    private String id;
    private String title;
    private String content;
    private boolean isPublished;
    private String author;
}

Warto zauważyć że nie ma tutaj klasycznych getterów, setterów i konstrukrotów, a wszystko dzieje się dzięki Lombok’owi i jego adnotacjom:

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor

Lecz najważniejsza dla nas jest adnotacja:

@Document(indexName = "blog", type = "post")

Pozwola ona na zapis obiektu „Post” do bazy danych Elasticsearch. Adnotacja „@Document” przyjmuje w tym wypadku dwa parametry:

  • indexName – nazwa unikalnego indeksu.
  • type – nazwa encji (unikalna w ramach indeksu)

Jeśli chcesz wiedzieć więcej na temat indeksowania danych w Elasticseach, to polecam ten post https://czterytygodnie.pl/sposoby-wyszukiwania-elasticsearch/

Kolejnym krokiem jest zaimplementowanie interfejsu rozszerzającego „ElasticsearchRepository”. Będzie on dostarczał nam predefiniowane metody, dzięki którym będziemy mogli w łatwy sposób zarządzać obiektem Post w bazie danych. Jak już wspomniałem wcześniej, jest to nic innego jak odpowiednik interfejsu „Repository” ze Spring Data.

package com.ara.elastic.repository;

import com.ara.elastic.model.Post;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface PostRepository extends ElasticsearchRepository<Post, String> {
}

Stwórzmy teraz serwis w formie interfejsu, który będzie dostarczał definicji metod do zarządzania obiektem Post.

package com.ara.elastic.service;

import com.ara.elastic.model.Post;

import java.util.List;

public interface PostService {

    String save(Post post);
    void delete(String postId);

    Post findById(String postId);
    List<Post> findAll();
    List<Post> findAllByPhrase(String phrase);
}

Implementacja tego serwisu może wyglądać następująco:

package com.ara.elastic.service.impl;

import com.ara.elastic.model.Post;
import com.ara.elastic.repository.PostRepository;
import com.ara.elastic.service.PostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.elasticsearch.index.query.QueryBuilders.regexpQuery;

@Service
public class PostServiceImpl implements PostService {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private ElasticsearchTemplate esTemplate;

    @Override
    public String save(Post post) {
        assignId(post);
        postRepository.save(post);
        return post.getId();
    }

    @Override
    public void delete(String postId) {
        Optional<Post> post = postRepository.findById(postId);
        post.ifPresent(post1 -> postRepository.delete(post1));
    }

    @Override
    public Post findById(String postId) {
        Optional<Post> post = postRepository.findById(postId);
        return post.orElse(null);
    }

    @Override
    public List<Post> findAll() {
        return ((PageImpl)postRepository.findAll()).getContent();
    }

    @Override
    public List<Post> findAllByPhrase(String phrase) {
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withFilter(regexpQuery("content", ".*" + phrase + ".*"))
                .build();
        return esTemplate.queryForList(searchQuery, Post.class);
    }

    private void assignId(Post post) {
        if (post.getId() == null)
            post.setId(UUID.randomUUID().toString());
    }
}

Mamy już logię biznesową nasze aplikacji, więc dodajmy jeszcze API aby można było jej używać 🙂

W poniższym kontrolerze są zdefiniowane metody do zapisu, usuwania i wyszukiwania obiektów typu Post.

package com.ara.elastic.controller;

import com.ara.elastic.model.Post;
import com.ara.elastic.service.PostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class PostController {

    @Autowired
    private PostService postService;

    @RequestMapping(value = "save", method = RequestMethod.POST)
    public String save(@RequestBody Post post) {
        return postService.save(post);
    }

    @RequestMapping(value = "all", method = RequestMethod.GET)
    public List<Post> findAll(){
        return postService.findAll();
    }

    @RequestMapping(value = "phrase", method = RequestMethod.GET)
    public List<Post> findByPhrase(@RequestParam("phrase") String phrase){
        return postService.findAllByPhrase(phrase);
    }

    @RequestMapping(value = "", method = RequestMethod.GET)
    public Post findById(@RequestParam("id") String id){
        return postService.findById(id);
    }

    @RequestMapping(value = "", method = RequestMethod.DELETE)
    public void deleteById(@RequestParam("id") String id){
        postService.delete(id);
    }
}

Podsumowanie

Reasumując, Elasticsearch doskonale sprawdzi się jako baza danych w aplikacjach, które przetwarzają duże ilości danych tekstowych i potrzebują robić to w jak najkrótszym czasie. Przesukiwanie pełnotekstowe jest tutaj ułatwione i przyspieszone dzięki indeksacji zasobów w Elasticsearch.


Poniżej znajdują się przydatne linki, wśród których znajdziesz też link do mojego Github’a i do projektu, użytego w tym poście.

https://www.elastic.co/
https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html
https://www.docker.com/
https://projectlombok.org/
https://github.com/radek-osak/spring-boot-elasticsearch
https://arasoftware.pl/2020/07/01/programowanie-reaktywne/

Zachęcam do zapoznania się z innymi postami na moim blogu, pozostawienie komentarza oraz udostępnienie tego wpisu jeśli Ci się spodobał!

Czy ten post był dla Ciebie pomocny?

Kliknij na gwiazdki żeby zagłosować.

Średnia ocena 3.5 / 5. Liczba głosów: 4

Brak głosów do tej pory. Bądź pierwszy(a)!

Bardzo mi przykro że ten post nie był dla Ciebie pomocny 🙁

Pozwól mi poprawić ten post!

Powiedz mi co mogę poprawić?