[삽질기록] 성능개선을 하고 싶어 2 (엘라스틱 서치와 LogStash)

케이·2022년 12월 18일
1

삽질기록

목록 보기
6/7
post-thumbnail

팀 프로젝트 기간 종료 후에도 학습을 위해 성능 개선을 진행하면서 엘라스틱 서치 자연어 검색을 도입한 과정을 짤막하게 남깁니다. 시간이 좀 지나 놓친 부분이 있을 수 있어 계속 업데이트 할 예정입니다. 혹시나 틀린 부분이 있다면 알려주시면 감사하겠습니다🙇🏻‍♀️

엘라스틱 서치란

100만은 큰 데이터도 아닌 요즘 시대에 빠른 검색 성능을 제공해야하는 건 필수적이다.
(첫 성능 테스트 시에 응답까지 5초가 걸린 것을 보면 끔-찍 하다. 참고 : 지난 포스트 - 성능 개선을 하고 싶었는데 말이죠)
일반적인 RDB 검색 속도는 O(N)인데 엘라스틱 서치의 경우 해시 테이블 구조의 역색인 방식으로 데이터를 구축 해두어 검색속도를 O(1)로 단축할 수 있다고 한다. 이에 상품 검색이나 본문 검색 등에 엘라스틱 서치가 사용되고 있다. (참고 https://seungwoolog.tistory.com/99)

왜 도입했나요??

nGrinder를 사용해 성능 측정을 하고 개선에 있어서 엘라스틱 서치의 자연어 검색 기능을 활용하면 성능 개선에 도움이 될 것이라고 판단해 도입해보았다.

여기서 집고 넘어가는 엘라스틱서치의 장점에는

  • 빠르고 효과적인 검색
  • 형태소 분석을 통한 자연어 처리
  • 대량 비정형 데이터 보관과 검색

등이 있는데 첫번째와 두번째 이유가 도입의 가장 큰 이유이다.

하지만 이 모든게 삽질의 시작이었으니…… ㅎ…

설정 과정

1. 엘라스틱 서치 설치

검색을 해보니 Docker-compose로 엘라스틱 서치를 설치하는 방법이 가장 많이 사용되는 것 같아 해당 방법을 따르기로 했다.

블로그에 나와 있는 방법들은.. 헷갈려서 공식문서를 보고 설치를 진행했다.

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.2.3
위의 명령어를 실행해 도커 이미지를 다운 받았다.

(추가 내용들은 잘 기억이 나질 않는다.. 기억 나는대로 추가 업데이트 할 예정.. 역시 실제로 했을 때 바로바로 기록해야한다..ㅠㅠ)

엘라스틱 서치를 다운 받았다면 우리의 작고 소중한 애플리케이션에서 사용할 수 있도록 설정 파일을 작성해야한다.

2. ElasticSearchConfig파일 작성

@EnableElasticsearchRepositories
@Configuration
public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {

    private String indexName;

    @Override
    public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter, RestHighLevelClient elasticsearchClient) {
        return new ElasticsearchRestTemplate(elasticsearchClient());
    }

    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo("localhost:9200")
                .build();
        return RestClients.create(clientConfiguration).rest();
    }

    @Override //매핑을 위해 추가적으로 넣어줌
    protected FieldNamingStrategy fieldNamingStrategy() {
        return new SnakeCaseFieldNamingStrategy();
    }

		//테스트 시와 애플리케이션 작동 시에 쓸 인덱스를 분리하기 위해 추가적으로 넣어줌
    public String getIndexName() {
        return indexName;
    }
}

3. Document 생성

엘라스틱 서치를 설치 했다면 색인화 작업을 위해 document (인덱스의 대상, 가지고 있는 필드들의 정보를 명시)를 추가해야한다.

@Getter
@Setter
@Builder
@AllArgsConstructor
@Document(indexName = "인덱스이름")
public class RentArticleDocument {

    @Id
    private Long id;
    @Field(type = FieldType.Text, name = "title")
    private String title;
    @Field(name = "is_completed")
    private boolean isCompleted;
    @Field(name = "is_deleted")
    private boolean isDeleted;

}

이렇게 엘라스틱 서치에 대한 설정을 한 뒤에 LogStash를 사용하기로 했는데… 그 이유는 DB에 있는 내용을 LogStash를 사용해 엘라스틱 서치 저장소에 넣어두기 위함이다.

LogStash

설치

LogStash도 설치하는 여러 방법이 있는데.. 이상하게도 직접 패키지를 다운로드 받아서 설치하는 것 이외에는 어떤 방법도 되질 않았다. 어쩔 수 없이 압축 파일을 다운 받아서 설치.

(https://www.elastic.co/kr/downloads/logstash)

1. Config 파일 작성

설치를 마쳤다면 LogStash의 설정 파일(.conf)을 작성해야 한다. 각 이벤트에 맞게 어떤 플러그인을 사용할 것인지 등등을 설정 파일에 작성해두면 된다. 일단 크게 input, filter, output 파트로 나뉘는데.. 해당 파일을 작성하는데는 내 설명보다.. 공식문서를 보는 것이 훨씬 좋을 것이라고 생각되어(사실이다) 아래 링크를 남긴다.
공식문서를 참고하세요

아래에는 열심히 삽질하며(같이 삽질해준 리에게 감사의 말을 전합니다)… 작성한 conf 파일 내용이다.

input {
    jdbc {
      jdbc_driver_library => "/Users/kl/logstash-8.5.1/bin/mysql-connector-java-8.0.28.jar"
      jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
      jdbc_connection_string => "DB주소"
      jdbc_user => "유저네임"
      jdbc_password => "비밀번호"
      jdbc_paging_enabled => true
      tracking_column => "unix_ts_in_secs"
      use_column_value => true
      tracking_column_type => "numeric"
      statement => "SELECT * from rent_article"
      schedule => "* * * * *" # Query주기 설정 
     	 }
      }
    
    # 필터가 필요하다면 아래 설정
    filter {
    	mutate {
    	copy => { "rent_article_id" => "[@metadata][_id]"}
    	convert => { "[@metadata][_id]" => "integer" }
    	remove_field => ["id", "@version", "unix_ts_in_secs", "@timestamp"]
        }
	}
    # 테스트를 위해 터미널에 output 설정
    output {
    	elasticsearch {
        hosts => ["127.0.0.1"]
    	index => "인덱스 이름 설정" #해당 이름으로 인덱스가 만들어지니 인덱스 1을 만들고
		#그 이후에 인덱스 2를 만들고 싶다면 여기서 이름을 설정하면 된다.
    	document_id => "%{[@metadata][_id]}"
	  }
	}

2. 인덱스 만들기

설정파일을 작성했다면 이후에 터미널에서

bin/logstash -f logstash.conf(설정파일이름) 을 실행하면 인덱스가 만들어진다!
만들어진 인덱스를 확인하고자 간단하게 Postman을 이용해 검색을 실행해봤다.
(예시 http://localhost:9200/인덱스이름/_search?필드명=검색어의 형태로 넣어주면 검색이 가능하다. 자세한 것은 역시나 공식문서를 참고하길 바란다)

지금은 아래에 나올 노리 토크나이저를 적용시킨 상태라서 이 당시의 api주소와는 조금 다르지만 성공하게 되면 아래처럼 결과를 받을 수 있다.

이렇게 일단 엘라스틱 서치가 잘 작동하는 것까지 확인했으니 그 다음 작업으로 자연어 검색을 위한 nori 플러그인을 사용해보도록 하자!

3. 자연어 검색 도입

엘라스틱서치의 플러그인으로 한국어 형태소 분석을 통해 자연어 검색을 가능하게 해주는 nori라는 플러그인이 있다. nori를 설치한 뒤에 생성하는 인덱스들이 nori를 사용할 수 있도록 설정을 해주기만 하면 된다.

먼저 자연어 검색이 무엇이냐? 예를 간단히 들자면..

"title": "동해물과 백두산이" 라는 제목이 있다고 했을 때
"동해물"로만 검색하면 해당 제목이 검색되지 않는다.
하지만 nori를 사용하면 제목이 '동해물', '과', '백두산', '이' 등등 형태소 단위로 분리되어 '동해물'만
검색했을 때도 위의 제목을 검색결과로 받을 수 있고 마찬가지로 '백두산'만 검색했을 때도
위의 제목을 검색 결과로 받을 수 있다.

자 그렇다면 또 설치를 해봐야 하는데..

sudo bin/elasticsearch-plugin install analysis-nori 명령어로 nori 플러그인을 설치할 수 있다. 보다 자세한 내용 공식문서에서 확인할 수 있다.

설치를 했다면 nori가 작동 하는지 확인하자! (참고 블로그)

위의 참고 블로그를 보고 차례차례 따라하다 보면 노리 토크나이저를 설정하고 동작하는 것을 확인할 수 있다.

다만..!! 여기서 주의할 점은!!! 노리 토크나이저를 활용하기 위해서 기존 생성했던 인덱스를 put으로 수정(수정 할 때 인덱스 open/close사용)해도 소용이 없다는 것..!!

처음부터 노리 토크나이저를 쓰는 인덱스를 생성해야 한다는 점을 잊지 말자!!

그래서 우리는 인덱스를 생성할 때 마다 노리 토크나이저가 포함 될 수 있도록 설정 파일을 작성했다!
요청주소: PUT http://localhost:9200/_template/템플릿명

{
"index_patterns" : [
      "noritest_*" //앞으로 noritest_~~ 시작하는 인덱스들은 아래의 설정을 따른다
    ],
    "order": 1,
    "settings": {
		"number_of_shards": 1,
		"number_of_replicas": 1,
		"index":{
			"analysis":{
				"tokenizer":{ //토크나이저 부분에 nori를 적용!
					"nori_mixed":{
						"type":"nori_tokenizer",
						"decompound_mode":"mixed"
					}
				},
				"analyzer":{
					"korean":{
						"type":"custom",
						"tokenizer":"nori_mixed",
						"char_filter":["html_strip"]
					}
				}	
			}	
		}
    },
//아래는 인덱스로 만들고자 하는 도메인의 필드 내용을 넣어주면 된다
   "mappings": { 
      "properties": {
        "address": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
      
        "is_completed": {
          "type": "boolean"
        },
        
        "title": {
          "type": "text",
					"analyzer":"korean",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
      }
    }

}

이후에 인덱스를 생성하면 인덱스에 토크나이저로 노리가 포함되어 있을 것이다!
인덱스를 생성한 이후에 nori가 잘 작동하는지 확인해보기 위해서
http://localhost:9200/인덱스이름/_analyze으로 요청을 보내보자!

형태소 분석이 제대로 작동하는 것을 확인 할 수 있다!

이후에 검색 결과를 애플리케이션의 도메인(or DTO) 형식으로 반환하기 위해 아래처럼 repository에 쿼리를 작성해주고 상황에 따라 필요한 로직들을 추가적으로 작성해주면 된다! (우리의 애플리케이션에는 검색 결과를 DTO로 변환해주는 로직을 추가해주었다)

public interface RentArticleDocumentRepository extends ElasticsearchRepository<RentArticleDocument, Long> {

    @Query("{\"bool\": {\"must\": [{\"match\": {\"title\" : \"?0\"}},{\"match\": {\"is_completed\": \"?1\"}}}]}}")
    List<RentArticleDocument> findByTitle(String title, boolean available, Pageable pageable);
}

일반 검색 요청도 보내보면

(위의 예시는 “역”이라는 제목으로 검색을 한 결과이다) 응답이 잘 오는 것을 볼 수 있다!!

느낀 점

정말 길고 긴 여정이었다.(글로 정리하니 그간의 노력과 삽질이 다 담기지 않는거 같아 아쉽지만.. ) 눈을 가린채 코끼리를 만지는 느낌이랄까.. 엘라스틱 서치를 적용하면서 ‘이게 맞나?? 제대로 하고 있는건가??’ 라는 생각을 정말 수도 없이 했던 것 같다. 처음부터 끝까지 정말 말그대로 하나하나 찾아가며 진행했다. (탭이 정말 수십개가.. 쌓였다.. 이 정도 쌓인 것은 처음...) 혼자라면 포기했을 수도 있었을 것 같은데 같이 고생해준 ‘리’(리의 블로그)가 있어서 어려웠지만 잘 해쳐 나갈 수 있었다. 다시 한번 리에게 고맙다는 말을 전하고 싶다.

이 외에 엘라스틱 서치가 작동되는 과정을 보면서 너무 신기했고 아직 어렵지만 공부할 리스트에 넣어놓고 실력이 조금 더 늘었을 때 작동 원리를 학습해보면 재밌을 것 같단 생각이 들었다. (지금은 봐도 뭔 소리인지 이해를 못하겠…ㅠㅠ) 앞으로도 파이팅!!

참고

https://tecoble.techcourse.co.kr/post/2021-10-19-elasticsearch/
https://backtony.github.io/spring/elk/2022-03-02-spring-elasticsearch-2/
https://kingname.tistory.com/225
https://velog.io/@soyeon207/Spring-Boot-에서-ES-사용하기-인덱스편
https://velog.io/@broccoliindb/mysql-데이터를-elasticsearch로-검색하기
https://myhappyman.tistory.com/218
https://doohee94.tistory.com/23
https://www.elastic.co/guide/en/kibana/8.5/docker.html
https://logz.io/blog/logstash-tutorial/
https://esbook.kimjmin.net/04-data/4.4-_search
https://discuss.elastic.co/t/elasticsearch-failed-to-install-template/239233
https://github.com/spring-projects/spring-data-elasticsearch/blob/main/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc
https://codecurated.com/blog/how-to-connect-java-with-elasticsearch/
https://esbook.kimjmin.net/02-install
https://velog.io/@broccoliindb/elasticsearch-index-생성-삭제-수정-노리-토큰-설정
https://anygyuuuu.tistory.com/14
https://www.gimsesu.me/elasticsearch-customize-tokenizer
https://seungwoolog.tistory.com/99
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori-tokenizer.html
https://victorydntmd.tistory.com/312

profile
삽질하며 깨닫고 배웁니다. (a.k.a 프로삽질러) + 이 구역의 회고왕

2개의 댓글

comment-user-thumbnail
2022년 12월 18일

선배님 진도가 너무 빠릅니다

1개의 답글