DBMS/ElasticSearch

ElasticSearch를 이용한 검색 최적화(with. SpringBoot)

블로그 주인장 2024. 3. 27.

Issue

프로젝트를 진행하면서 태그 검색 을 하는 기능을 구현해야했습니다.

태그 조회 API에서 구현하고자 하는 기능은 다음과 같습니다.

  1. 태그 검색 기능
  2. 태그 자동 완성 기능

해당 조건을 구현하고자 할 때는 기존 RDB 를 사용할 수 있습니다.

검색할 때 RDB의 Like 검색 기능을 사용해도 되지만, Table Full scan 방식을 사용하고 있기 때문에

데이터의 양이 많아질수록 검색의 속도가 현저히 감소하게 됩니다.

이를 통해 해결하기 위해 ElasticSearch 를 활용하여 검색하는 기능을 최적화를 진행해보려고 합니다.

자세한 ElasticSearch 내용은 필자가 정리한 블로그에서 확인하면 됩니다. => 블로그 정리 링크

 


Problem

ElasticSearch 역색인 기반의 검색엔진으로 검색 속도가 빠르고,

NoSQL 의 형식의 데이터를 저장하기 때문에 NoSQL 데이터베이스라고도 불린다.

 

위에서 말한 역색인 은 특정 단어가 어느 문서에 있는지 기록하기에 특정 단어를 검색할 때,

모든 문서를 훑지 않고 검색하여 찾을 수 있습니다.

 

도커 파일 설정

ElasticSearch 를 설치하기 위해 Docker 를 사용해보겠습니다.

  • dockerfile
    ARG VERSION
    FROM docker.elastic.co/elasticsearch/elasticsearch:${VERSION}
    RUN elasticsearch-plugin install analysis-nori​
  • docker-compose.yml
    elasticsearch:
        build:
          context: ./elastic_search
          dockerfile: Dockerfile
          args:
            VERSION: 8.6.2
        container_name: elasticsearch
        environment:
          - cluster.name=es-docker
          - node.name=es01
          - discovery.type=single-node
          - xpack.security.enabled=false
        ulimits:            #컨테이너 리소스 제한
          memlock:          #메모리 잠금 제한
            soft: -1
            hard: -1
          nofile:           #열려 있는 파일 설명자의 최대 갯수
            soft: 65536
            hard: 65536
        cap_add:
          - IPC_LOCK        #메모리를 잠글 수 있는 기능
        ports:
          - "9200:9200"     #엘라스틱 서치가 접속하는 포트
          - "9300:9300"     #내부 노드가 통신하는 포트
        volumes:
          - elasticsearch-data:/usr/share/elasticsearch/data
        networks:
          - withcon
        restart: always
    
      kibana:
        container_name: kibana
        image: docker.elastic.co/kibana/kibana:8.6.2
        environment:
          ELASTICSEARCH_HOSTS: http://elasticsearch:9200
        ports:
          - "5601:5601"
        depends_on:
          - elasticsearch
        restart: always
        networks:
          - withcon
    
    volumes:
      elasticsearch-data:​

 

docker-compose.yml 파일을 보면 ElasticSearch 서비스를 실행하기 위해서 Dockerfile 을 사용하고 있습니다.

따로, nori 플러그인을 설치하기 Dockerfile 을 만들게 되었습니다.

더보기

💡 nori 플러그인
 - 한글 형태소 분석기인 nori 플러그인을 설치하면 한글을 분석할 수 있습니다.

 

ElasticSearch 설정

ElasticSearch 을 연동하기 위해서 의존성 및 Config 파일을 설정합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
}

 

ElasticSearhConfig

@Configuration
@EnableElasticsearchRepositories
public class ElasticSearchConfig extends ElasticsearchConfiguration {

  @Override
  public ClientConfiguration clientConfiguration() {
    return ClientConfiguration.builder()
        .connectedTo("localhost:9200")
        .build();
  }
}

 

ElasticSearch는 9200번 포트를 기본적으로 사용하기 때문에

해당 포트로 연결할 수 있도록 설정 파일을 구현해줍니다.

 

@EnableElasticsearchRepositories 어노테이션을 이용하여 ElasticSearchRepository 를 사용한다는 것을 설정합니다.

 


Solve

ElasticSearch 에서 데이터를 색인하는 방법은 2가지가 존재합니다.

  1. Logstash 를 사용하여 RDB에서 데이터를 가지고 와서 ElasticSearch 색인
  2. SpringBoot 에서 실시간으로 ElasticSearch 색인

Logstash 를 이용하여 현재 프로젝트에서 사용 중인 RDB인 MySQL 로 사용하려했지만,

별도의 서버가 필요해서 관리비용이 높고 러닝커브가 있기에

간단하고 직관적인 방법인 후자 로 진행해보겠습니다.

 

서비스 코드 구현

태그를 검색하기 위해서 태그를 생성하는 서비스인 채팅방 생성하는

서비스에 ElasticSearch 를 이용할 수 있도록 색인을 진행합니다.

private void upsertTagSearch(List<Tag> tagList) {
    List<TagSearch> searches = tagList.stream()
        .map(tag -> {
          TagSearch tagSearch = tagSearchRepository.findByName(tag.getName()).orElse(null);

          if (tagSearch == null) {
            return TagSearch.builder()
              .id(tag.getId().toString())
              .name(tag.getName())
              .tagCount(1)
              .build();
          } else {
            Integer count = tagRepository.countTagByName(tag.getName());
            tagSearchRepository.updateTagCount(tagSearch.getId(), tag.getName(), count);

            return TagSearch.builder()
                .id(tag.getId().toString())
                .name(tag.getName())
                .tagCount(count)
                .build();
          }
        }).toList();

    tagSearchRepository.saveAll(searches);
  }

 

upsertTagSearch 메서드에 대해 알아보겠습니다.

  • request 로 입력받은 tagList 에서 기존 tag 데이터베이스에 저장되어있는지 확인합니다.
  • 해당 tag 를 새롭게 저장하는 경우 ElasticSearch 에 색인합니다.
  • 해당 tag 가 새롭게 저장되어 있는 경우는 태그가 저장되어있는 count 를 확인해서
    ElasticSearch 에 있는 데이터를 업데이트를 진행합니다.

 

ElasticSearch Document & Repository

@Getter
@Document(indexName = "tag")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Mapping(mappingPath = "static/elastic/tag-mappings.json")
@Setting(settingPath = "static/elastic/elastic-settings.json")
public class TagSearch {
  @Id
  private String id;

  @Field(type = FieldType.Text, name = "name")
  private String name;

  @Field(type = FieldType.Integer, name = "tag_count")
  private Integer tagCount;
}

 

위의 코드는 Document 를 구현한 코드입니다.

해당 코드 상에서 기존 JPA 를 사용할 때와 다른 어노테이션에 대해 알아보겠습니다.

 

ElasticSearch 어노테이션

@Document

  • ElasticSearch에서 사용될 인덱스 이름(Table 이름)

@Mapping

  • ElastsicSearch에서 색인할 때 사용될 매핑 파일 경로
  • 매핑 파일은 ElasticSearch 에 색인될 때 사용될 데이터 타입을 정의 합니다.
  • 따로 매핑 파일을 작성하지 않고, @Field 어노테이션을 사용해도 됩니다.

@Setting

  • ElasticSearch에서 색인될 때 사용될 세팅 파일 경로
  • 세팅 파일은 ElasticSearch 에 색인될 때 사용될 인덱스 세팅을 정의합니다.
  • 예시로, nori 플러그인을 사용하기 위한 analysis(분석기) , tokenizer 세팅이 가능합니다.
@Repository
public interface TagSearchRepository extends ElasticsearchRepository<TagSearch, String>,
    CustomTagSearchRepository {

  List<TagSearch> findAllByNameStartingWithIgnoreCase(String name, Pageable pageable);
  
  Optional<TagSearch> findByName(String name);

}

Document 클래스를 사용하기 위한 Repository 생성한 코드입니다.

마치 JPA를 사용할 때 Repository를 생성하는 것과 비슷합니다.

JpaRepository 대신 ElasticsearchRepository 를 상속 받습니다.

이 인터페이스에 기본적인 CRUD 메서드가 정의되어있고, JPA의 JPQL처럼 메서드 이름을 통해 쿼리를 생성할 수 있습니다. 또는 @Query 어노테이션을 사용하여 직접 쿼리를 작성할 수도 있습니다.

 

ElasticSearch 검색 API 구현

@Override
@Transactional(readOnly = true)
public List<TagSearchDto> findTagKeyword(String keyword) {
  Pageable pageable = PageRequest.of(0, 10, Sort.by(Direction.DESC, "id"));

  List<TagSearch> tagSearches = tagSearchRepository.findAllByNameStartingWithIgnoreCase(keyword, pageable);
  return tagSearches.stream().map(TagSearchDto::fromEntity).toList();
}

위의 코드는 태그를 검색하는 서비스입니다.

 

페이징 처리를 진행하여, keyword 를 매개변수로 받아서

ElasticSearch 에 해당 keyword 로 시작하는 tag를 조회하도록 구현했습니다.

 

API TEST

API HTTP 요청 프로토콜

 

인텔리제이의 HTTP 테스트를 이용하여 태그를 검색한 예시입니다.

이라는 단어를 검색을 해보겠습니다.

결과 반환값

 

해당 프로토콜을 실행하면 ElasticSearch 에서 검색하여 현재 DB 상에 있는 영 이라고 시작된 키워드를 조회하고,

해당 키워드를 사용하고 있는 태그의 개수도 동일하게 출력하게 됩니다.

 

RDBM vs ElasticSearch

간단하게 태그를 동일하게 검색하는 기능을 하는 API를 구현하여 성능 차이를 비교해보겠습니다.

MySQL vs ElasticSearch

 

위의 이미지를 확인하면 동일한 키워드인 영 을 검색한 API입니다.

RDB인 MySQL 을 사용할 때와 ElasticSearch 를 사용했을 경우의 시간을 확인하면

기존 검색 시간 대비 약 5배 정도 시간이 줄어든 것을 확인할 수 있습니다.

 


What I Learn

ElaticSearch 는 역색인 구조를 가지고 있기 때문에 특정 키워드에 대한 검색을 모든 문서를 훑지 않고 찾을 수 있다는 것을 알게 되었고, 큰 테스트는 아니지만 기존 RDB 와의 테스트 시간을 확인했을 때 확연하게 파악이 가능했다.

현재, 실시간으로 ElasticSearch 에 저장해서 사용하는 방법을 구현했는데, 차후에는 Logstash 를 활용하여 MySQL 과 연결하여 해당 DB에 있는 데이터를 검색하여 사용할 수 있도록 구현해보고싶다.

반응형

'DBMS > ElasticSearch' 카테고리의 다른 글

ElasticSearch란 무엇인가요?  (0) 2024.02.06

댓글