DHistory
ElasticSearch vs MySQL FullText vs MySQL Like Performance 본문
개요
대용량 데이터 검색을 진행함에 있어 ElasticSearch, MySQL FullText, MySQL Like의 성능을 비교를 했습니다.
검색을 진행한 조건은 다음과 같습니다.
1. "abc" 로 시작하는 단어가 포함된 경우 → "def abcd gcf"
2. "abc" 문자가 포함된 경우 → "def dabc gcf"
Performance 비교는 hey를 활용하였습니다.
Table Schema
현재 데이터 건수는 275,684건이며 title에 index / fulltext 가 정의되어있습니다.
Create Table: CREATE TABLE `post` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '(AI) ID',
`title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`contents` varchar(3000) COLLATE utf8mb4_unicode_ci NOT NULL,
`image_url` varchar(2083) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_post_m2` (`title`),
FULLTEXT KEY `idx_post_m1` (`title`)
) ENGINE=InnoDB AUTO_INCREMENT=275685 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
MySQL Like
가장 구현하기 쉬운 Like 구문입니다.
하나의 Like 구문으로 Case 1, 2를 모두 만족할 수 있습니다.
explain
select *
from post
where title like '%abc%';
%abc%로 진행했기 때문에 index를 활용할 수 없어 Full Scan을 진행했습니다.
@Service
@RequiredArgsConstructor
public class SearchService {
private final PostJpaRepository postJpaRepository;
public List<PostResponse> database(String keyword) {
List<Post> posts = postJpaRepository.findByTitleContaining(keyword);
return posts.stream()
.map(post -> new PostResponse(post.getTitle(), post.getContents()))
.toList();
}
}
가장 간단히 구현했지만 API 호출 시 Latency가 5초 이상 소요되었습니다.
다음으로 성능 테스트를 진행해보겠습니다.
Hey를 이용하여 성능 테스트 진행
3000건의 요청 중 14건만 성공하였습니다.
이 외 2986건은 Timeout(30초)으로 인해 요청이 실패했습니다.
성공한 요청에 대해서도 P90 Latency가 거의 20초 가량 소요됩니다.
데이터가 많아질수록 사용하기 어렵습니다.
MySQL FullText
FullText 사용 전 Index 생성 과정을 알아야합니다.
MySQL에서 기본적으로 제공하는 과정은 ngram을 활용하여 단어를 Index를 생성합니다.
→ ngram은 단어 단위 (Whitespace) 로 구분합니다.
그 결과
Case 1을 검색할 수 있지만, Case2는 검색이 불가능합니다.
상황에 따라 특정 단어가 들어간 경우만을 찾을 수 있기 때문에 성능을 확인해보겠습니다.
explain
select *
from post
where match(title) against('abc*' in boolean mode);
FullText 기반으로 검색하기 때문에 index를 활용할 수 있습니다.
@Service
@RequiredArgsConstructor
public class SearchService {
private final PostJpaRepository postJpaRepository;
public List<PostResponse> fullTextDatabase(String keyword) {
List<Post> posts = postJpaRepository.findByTitleFullText(keyword + "*");
return posts.stream()
.map(post -> new PostResponse(post.getTitle(), post.getContents()))
.toList();
}
}
public interface PostJpaRepository extends JpaRepository<Post, Long> {
@Query(value = "SELECT * FROM post WHERE MATCH (title) AGAINST (?1 IN BOOLEAN MODE)", nativeQuery = true)
List<Post> findByTitleFullText(String keyword);
}
Like 방식 보다는 복잡하지만 API 호출 시 Latency가 167ms가 소요되었습니다.
다음으로 성능 테스트를 진행해보겠습니다.
Hey를 이용하여 성능 테스트 진행
전체 요청이 성공했습니다.
성공한 요청 중 P90 Latency가 거의 916ms 가량 소요되었습니다.
(현재 요청은 랜덤한 문자 2글자에 대한 요청입니다.)
ElasticSearch
ElasticSearch의 Tokenizer는 기본으로 Standard Token을 사용합니다.
Whitespace를 기준으로 Token화 하는 과정은 FullText Index 생성시와 유사합니다.
다만, ElasticSearch로 검색 시 BM25(Best Matching 25) 알고리즘을 사용하여 검색합니다.
GET /_search
{
"query": {
"wildcard": {
"title": {
"value": "*abc*",
"case_insensitive": true
}
}
}
}
wildcard 방식을 활용하여 MySQL Like와 비슷하게 Case 1, Case 2 방식을 모두 만족합니다.
@Service
@RequiredArgsConstructor
public class SearchService {
private final ElasticsearchTemplate elasticsearchTemplate;
public List<PostResponse> elasticSearch(String keyword) {
Query title = QueryBuilders.wildcard()
.field("title")
.value("*" + keyword + "*")
.caseInsensitive(true)
.build()
._toQuery();
NativeQuery query = new NativeQueryBuilder()
.withQuery(title)
.withMaxResults(100)
.build();
SearchHits<PostDocument> search = elasticsearchTemplate.search(query, PostDocument.class);
List<SearchHit<PostDocument>> searchHits = search.getSearchHits();
return searchHits.stream()
.map(this::convertPostResponse)
.toList();
}
private PostResponse convertPostResponse(SearchHit<PostDocument> searchHit) {
PostDocument post = searchHit.getContent();
return new PostResponse(post.getTitle(), post.getDescription());
}
}
FullText 방식 보다는 복잡하지만 API 호출 시 Latency가 245ms가 소요되었습니다.
다음으로 성능 테스트를 진행해보겠습니다.
Hey를 이용하여 성능 테스트 진행
전체 요청이 성공했습니다.
성공한 요청 중 P90 Latency가 거의 490ms 가량 소요되었습니다.
(현재 요청은 랜덤한 문자 2글자에 대한 요청입니다.)
🧐 그렇다면 Case 1의 경우 FullText와 비교하여 ElasticSearch가 항상 빠른걸까요?
답은 그렇지 않다입니다.
위 예제들에서는 text 길이를 2로 설정했습니다.
MySQL의 설정을 살펴보면 ft_min_word_len의 길이를 확인할 수 있습니다.
최소 길이가 4이기 때문에 FullText의 성능을 최대로 발휘할 수 없었습니다.
그럼 랜덤 문자 길이를 4글자 요청으로 성능 테스트를 진행해보겠습니다.
FullText vs ElasticSearch
FullText | ElasticSearch | |
TPS | 2406 | 341 |
P90 Latency | 0.074 | 0.309 |
FullText의 Performace가 높은 내용을 확인할 수 있습니다.
Learning Point
1. 단어가 포함된 검색이라면 ElasticSearch를 활용
➢ Like: 5370ms / ElasticSearch: 496ms
2. 단어로 시작하는 경우라면 FullText를 고려
➢ FullText: 74ms / ElasticSearch: 309ms
'Infrastructure' 카테고리의 다른 글
FanOut을 활용하여 NewsFeed Performance 향상 (0) | 2024.11.03 |
---|