본문 바로가기
프로젝트

검색 API에 인덱스, Elastic Search 적용하기

by eunyoung 2023. 3. 19.

검색 API 성능 개선하기


검색 API 성능을 개선하기 위해서는 여러가지 방법이 있다.

 

이번에 스프링 기반의 프로젝트를 진행하면서 검색 API 성능 개선을 시도했는데

이 과정에서 Index 추가 더 나아가 Elastic Search 도입을 해보았다. 

이 과정에 많은 고민과 시행착오를 겪었고 이에 대해서 정리해보았다.

 

 

검색 API가 활용되는 프론트엔드 화면

 

아래는 검색 API가 활용되는 프론트엔드 화면이다.

카테고리를 선택하고 검색어를 검색하면 검색어에 해당하는 크롤링 컨텐츠가 나오는 기능이다

 

 

 

 


검색 API를 개선을 하려는 이유


이 프로젝트의 검색 API는 크롤링 컨텐츠를 기반으로 검색이 이루어진다.

크롤링 컨텐츠들이 있고 검색어가 입력되면 그 크롤링 컨텐츠들의 제목 기반으로 매칭해서 검색 컨텐츠를 제공하는 것이다.

 

프로젝트에서 검색 API가 이용되는 부분

 

하지만 크롤링 컨텐츠들이 약 10만개 정도 존재하고, 점점 DB에 더 쌓이게 되면서 문제가 발생하였다.

 

  • 검색 API 처리속도가 현저히 낮아짐.

  • 처리 실패율도 50% 정도를 넘기는 것과 같이 실패율도 매우 높음.

다음 그림은 ngrinder로 성능 부하 테스트를 한 결과이다.

 

 

이 프로젝트에서는 20분 간격으로 크롤링 서버를 호출해서 최신 크롤링 컨텐츠를 가져온다.
따라서 주기적으로 RDB에 크롤링 컨텐츠가 쌓인다.

그러므로 추후에 DB에 크롤링 컨텐츠가 계속 쌓일수록 위에서 발생한 문제는 더 문제가 될 것이라고 생각했다

 

 

 


문제점 분석과 인덱스 고려


검색 쿼리는 매 요청시마다 테이블을 풀 스캔한다.

데이터가 20만개가 넘는 테이블을 매 요청마다 풀 스캔하는 것이다.

 

이게 제일 큰 문제점이라고 생각했다.

 

따라서 이것을 해결하기 위한 해결책으로 처음 고려했던 것은 인덱스를 추가해주는 것이었다,

 

평소에 인덱스가 테이블에서 데이터의 조회 방식을 개선시켜주는 것이라고 알고 있어서 이 방법을 먼저 고려했다.

 

 


인덱스 추가하기


검색 API의 쿼리문은 다음과 같다.

select * from 크롤링 컨텐츠 where 크롤링 컨텐츠.title like '%검색어%';

 

검색어 API에서는 LIKE %검색어%로 쿼리가 이루어진다.

해당 검색어가 포함된 값 모두를 조회해야하므로 위와 같이 쿼리문이 나간다.

 

검색이 title 기준으로 이루어지므로 크롤링 컨텐츠 테이블(YoutubeAndNews, Job 테이블)의 title 컬럼에 인덱스를 추가해주면 된다고 생각했고 title 컬럼에 인덱스를 적용해주었다.

 

 

 


인덱스 추가 방식의 한계점


하지만, 인덱스 추가 방식은 2가지 한계점이 존재하였다.

  • LIKE에서 %의 위치에 따라서 index를 타지 않는 이슈가 발생

  • B Tree 정렬이 자주 일어나게 된다는 문제가 발생

 

 

첫번째로는, LIKE에서 %의 위치에 따라서 index를 타지 않는 이슈가 발생했다.

 

인덱스를 적용했는데 풀스캔으로 조회가 되었다.

 

첫번째 한계점에 대한 원인은 다음과 같다.

 

(1) 인덱스를 걸면 해당 컬럼은 정렬이 된다.

 

(2) 여기서는 title 컬럼에 인덱스를 추가한 것이므로 title 기준으로 해당 컬럼이 정렬된다.

 

(3) 조회 쿼리가 요청되어서 조회를 하면 title 기준으로 앞부분부터 검색해야 하는데, LIKE절이 '%'로 시작하면 앞 문자를 모르니 인덱스를 사용할 수 없게 되는 것이다.

 

 

 

두번째로는, 인덱스 기준으로 되어 있는 B Tree 정렬이 자주 일어나게 된다는 문제가 있다.

 

인덱스는 변경이 잦지 않은 데이터에 적용해야 처리속도 측면에서 효과를 보는 것으로 알고 있다.

즉, 데이터 변경이 일어나면 인덱스의 B Tree도 다시 정렬이 되어야 하므로,  insert, update, delete가 자주 일어나는 곳에서는 인덱스를 적용하는 것이 적합하지 않다는 것으로 알고 있었다.

 

하지만, 이 프로젝트에서는 20분 간격으로 최신 컨텐츠가 RDB에 추가가 된다.
따라서 처리속도를 개선시키려고 인덱스를 적용한 것인데, 인덱스로 인해서 20분 간격으로 인덱스를 기준으로 다시 정렬하는 작업이 발생하는 것은 매우 비효율적이라고 생각했다.

 

따라서, 위의 두가지 이유로 인덱스를 추가해서 해결하는 방법은 하지 못하게 되었다.

 

 

 


FullText Search란?


이후, 다른 해결책을 찾아보려고 검색을 해보니 생각보다 like %검색어% 문제로 검색 쿼리에 인덱스를 걸지 못하였다는 블로그 글들을 많이 있었고, 여기서 이에 대한 해결책으로 FullTextSearch라는 것을 알게 되었다.

 

[MySQL FullTextSearch 란?]

 

MySQL InnoDB 5.6 버전부터 전문 검색을 위해서 등장한 것이다.

MATCH AGAINST 키워드를 통해서 이용되고, 이것을 사용하기 위해서는 테이블에 FullText Index가 추가되어야 한다.

 

FullTextSearch는 첫 글자 뿐만 아니라 중간의 단어로도 인덱스를 생성해 주어서, 인덱스 효과를 누릴 수 있었다.

 

[MySQL FullTextSearch 인덱스와 일반 인덱스의 차이점]

 

일반 인덱스는 인덱스를 걸어준 컬럼의 데이터를 바탕으로 인덱스를 걸어준다.

반면, MySQL FullTextSearch에서는 인덱스를 걸어준 컬럼의 데이터의 단어 하나하나를 기준으로 인덱스를 걸어주는 것이 가능하다.

따라서 일반 인덱스에 비해서는 인덱스가 많아지나 처리속도 면에서는 확실히 이점이 있다고 한다.

 

 

 


FullText Search 적용하기


MySQL FullTextSearch는 공백을 기준으로 단어를 저장하기 때문에 정확히 검색 단어가 일치해야만 인덱스 효과를 누릴 수 있었다.

 

따라서, 이 한계를 해결하기 위해서 n-gram Parser를 적용해주어야 한다.

n-gram Parser는 설정한 n의 값의 단위로 쪼개어 인덱스를 모두 저장한다. n-gram token size의 기본값은 2이다.

 

 

밑의 코드와 같이 ngram parser를 적용해서 크롤링 컨텐츠의 fulltext search 인덱스를 적용해주었다.

ALTER TABLE 크롤링 컨텐츠 ADD FULLTEXT INDEX 크롤링 컨텐츠_fulltextsearch (title) WITH PARSER ngram;

 

또, MySQL FullTextSearch에는 StopWords를 제공한다. 여기에는 'a','for' 또는 'to'와 같은 단어들이 저장되어 있어서 이 단어들은 검색이 되지 않는다.

따라서 이 단어들까지 검색하고 싶으면 StopWords 설정을 바꾸어주면 된다.

 

나는 위의 쿼리문에 나온 것과 같이 fulltext search 인덱스를 생성해주고,

Spring Data JPA에서 밑의 그림과 같이 설정해서 MySQL FullTextSearch라는 것을 사용해서 검색 API를 개선을 시도하였다.

 

 

 

이후, JOB엔티티를 조회하는 API에 ngrinder를 이용해서 성능 부하 테스트를 해주었다.

동일한 JOB 테이블의 컬럼 개수에, 동일한 검색어를 적용해서 테스트한 결과이다.

fulltextsearch를 적용한 후 TPS는 거의 2배 이상 증가, 처리시간은 1/2정도로 감소, 그리고 에러 발생율도 1/5 정도 감소하였다..


즉, 위에서 발생한 문제점을 어느정도 해결할 수 있었다.

 

하지만 FullText Search를 이용하는 것도 한계점이 있었다.

 

크롤링 데이터를 많이 쌓아두고 가상의 유저를 더 늘려서 부하테스트를 진행시 처리속도가 매우 낮아지고 에러 발생율이 50%이상을 넘어갔다.

 

따라서 다른 해결 방법을 고려했는데 다른 저장소를 사용하는 것이었다.

 

 


Elastic Search란?


Elastic Search는 루씬(Lucene)기반으로 텍스트, 숫자, 위치 기반 정보, 정형 및 비정형 데이터 등 모든 유형의 데이터를 위한 무료 검색 및 분석 엔진이고, 역색인이라는 자료구조를 사용하는데, 이는 전문 검색에 있어서 빠른 성능을 보장한다.

 

 

MySQL의 fullTextSearch에서 Elastic Search를 사용하는 것으로 넘어가게 되면서, MySQL과 Elastic Search를 비교해보면서 정리해보았다.

 

  • MySQL은 데이터베이스 관리 시스템 자체이지만, Elastic Search는 검색 엔진 자체일뿐이다.

  • Elastic Search는 샤드를 사용한 분산 시스템 지원이 가능하다.

  • Kibana라는 Elastic Search를 시각화하고 관리하기 위한 도구가 지원된다.

 

Elastic Search는 역색인 방식으로 조회하므로 RDB에 비해서는 빠른 검색이 가능하고, 또 Scale-out 하기에 편리한 것도 장점이라고 생각했다.

 

따라서 처리속도 면이나 추후 서비스가 확장될 것을 고려하면 Elastic Search 도입이 더 좋을 것이라고 판단했다.

 

 

 


Elastic Search 적용하기


적용 과정을 간략하게 요약하면 다음 그림과 같다.

 

1. spring-data-elasticsearch 의존성을 추가해주고, 

 

2. Elastic Search 관련 엔티티를 추가해준다.

 

3. 또, ElasticSearchRepository를 상속한 Repository단 인터페이스를 만들어준다.

 

4. 이후, Service와 Controller 단에서 코드 작성하는 것은 동일하다.

 

 

 


Elastic Search과 RDB


크롤링 컨텐츠를 저장하는 저장소로 RDB와 Elastic Search 꼭 총 두가지를 사용할 필요가 있나라는 생각을 했다.

 

하지만 검색해서 찾아보니 Elastic Search는 트랜잭션을 지원을 안해준다는 것을 알게 되었다.

즉, 트랜잭션 롤백 처리 같은 기능을 이용할 수 없는 것이다.

 

따라서 RDB와 Elastic Search 저장소 2가지를 사용하는 것이 맞다고 판단했다.

 

 


Elastic Search를 도입으로 인한 이슈 발생


하지만 이 방식에서는 RDB와 Elastic Search 즉, 데이터 저장소가 2개가 존재하면서 RDB와 Elastic Search 사이에 데이터 동기화 문제가 발생하였다.

최신 크롤링 컨텐츠는 RDB로 Insert가 되므로 이를 검색 API 요청시 데이터를 가져오는 Elastic Search에 빠른 시간내에 반영을 해줘야 하는 것이다.

 

이 문제를 해결하기 위해서 여러가지 해결 방안에 대해서 생각해보았고, 결과적으로 최종적으로 적용한 해결책은

배치서버의 주기적인 호출 이용 + RDB에 Elastic Search에 색인이 되었는지 안되었는지 나타내는 컬럼 추가 이다.

 

즉, 해당 이슈에 대한 해결 과정은 다음과 같다.

 

(1) RDB의 검색 대상이 되는 크롤링 컨텐츠 엔티티에 Elastic Search에 색인이 되었는지 안되었는지(READY or DONE) 나타낼 수 있는 컬럼을 추가한다.

 

READY(ES에 색인 X), DONE(ES에 색인 O)을 나타낼 수 있는 컬럼을 추가

 

(2) 애플리케이션 서버에 크롤링 컨텐츠 중에서 READY로 되어 있는 데이터를 ES에 색인하고 DONE으로 바꾸어주는 API를 추가한다.

 

(3) 배치서버에서 @Scheduled를 통해서 10분 간격으로 애플리케이션 서버에 ES에 색인하는 API (이 프로젝트에서는         /api/contents/elasticsearch)를 호출한다.

 

(4) 배치서버의 요청을 받은 애플리케이션 서버는 RDB에서 'READY'값을 가지는 컨텐츠 데이터들만 ES에 색인하는 작업을 진행한다.

 

(5) 위 (3)의 작업이 성공적으로 마무리되면 애플리케이션 서버는 ES에 색인 완료된 데이터들을 RDB에 'DONE'으로 바꿔준다.

 

(6) 클라이언트에서 검색 API 요청시에는 RDB가 아닌 Elastic Search에서 데이터를 가져오도록 한다.

 

 

위의 전체 프로세스를 그림으로 나타내면 다음과 같다.

위 과정을 통해서 Elastic Search와 RDB의 데이터 동기화 문제는 해결하고 검색 API 요청시 성공적으로 검색 데이터를 ES에서 가져오도록 할 수 있었다.

 

 


Elastic Search를 도입한 결과


검색 API에 Elastic Search 도입후, ngrinder를 이용해서 부하테스트를 진행한 결과,

TPS는 5배 정도 증가하였고, 처리시간은 5배 정도 감소하였다.

 

 

 

참고