검색 기능은 모든 애플리케이션에서 사용자 경험의 핵심 요소이다.
용자가 특정 키워드를 검색했을 때 원하는 결과를 얻지 못하면, 다른 키워드로 다시 검색해야 하는 불편함을 겪게 된다.
따라서, 사용자가 원하는 도서를 빠르고 정확하게 찾을 수 있도록 하는 것은 서비스의 중요한 요소라고 생각한다.
이 프로젝트에서는 도서 검색 기능을 구현하기 위해 여러 가지 기술적 선택지를 검토하였고,
그 중 MySQL의 FULLTEXT 인덱스를 활용하기로 결정했다.
왜 이 선택을 하게 되었는지, 그리고 그 과정에서 고려한 요소들을 설명하고자 한다.
도서 검색에서 고려해야할 요소
- 검색 속도: 사용자가 빠르게 결과를 얻기 위한 검색 속도
- 검색 정확도: 사용자가 원하는 결과를 정확하게 얻을 수 있어야 한다.
- 다양한 검색 기준: 제목, 저자, ISBN, 출판사, 키워드 등 다양한 필드를 기준으로 검색할 수 있어야 한다.
- 복잡한 쿼리 지원: 여러 조건을 조합한 검색이 가능해야 한다.
이러한 요구사항을 고려했을 때, 검색 기능 구현 종류는 여러가지가 있다.
1. LIKE 연산자
- 장점: 간단하고 구현이 쉬움
- 단점: 대량의 데이터에서 성능이 저하될 수 있음. 단어의 일부분만 매칭되는 경우를 다루기 어려울 수 있음
- 추천 상황: 소규모 데이터베이스 또는 초기 프로토타입 단계
2. FULLTEXT 인덱스 및 MATCH ... AGAINST
- 장점: 대량의 텍스트 데이터에서 빠른 검색 성능 제공. 자연어 검색 지원
- 단점: 세부 튜닝이 필요할 수 있으며, 모든 MySQL 버전과 스토리지 엔진(MyISAM, InnoDB)에서 사용 가능한 것은 아님
- 추천 상황: 도서의 제목, 설명, 키워드와 같은 텍스트 필드에서 고성능 검색이 필요한 경우. 특히 대규모 데이터셋에서 유용함
ALTER TABLE books ADD FULLTEXT(title, author, description);
SELECT * FROM books
WHERE MATCH(title, author, description) AGAINST('search_term');
3. Elasticsearch같은 외부 검색 엔진 사용
- 장점: 매우 강력한 검색 기능과 확장성 제공. 고급 텍스트 분석, 순위 지정, 다중 언어 지원 가능
- 단점: MySQL과 별도로 설치 및 관리해야 하며, 복잡성이 증가할 수 있음
- 추천 상황: 대규모 애플리케이션에서 높은 검색 성능과 유연성을 요구할 때. MySQL만으로는 검색 성능이 충분하지 않을 때
4. 복합 검색
여러 조건을 조합하여 보다 정교한 검색을 수행할 수 있다. 예를 들어, 사용자가 제목과 저자 모두를 기준으로 검색할 수 있다.
SELECT * FROM books
WHERE (title LIKE '%search_term%'
OR author LIKE '%search_term%')
AND publication_year = '2023';
요약
- 규모가 작은 데이터베이스에서 기본적인 검색 기능을 원할 경우: LIKE 연산자를 사용하여 구현할 수 있다.
- 규모가 크고 검색 성능이 중요한 경우: FULLTEXT 인덱스를 사용하는 것이 좋다. 특히, 다양한 텍스트 필드에 대해 자연어 기반의 검색이 필요하다면 유용하다.
- 더 복잡하고 강력한 검색을 원할 경우: Elasticsearch와 같은 전문 검색 엔진을 MySQL과 함께 사용하는 것이 좋다.
결정
FULLTEXT 인덱스를 활용하기로 했다.
첫번째 이유는 익숙한 환경이 아무래도 가장 큰 이유를 차지한다.
Elasticsearch는 학원에서 경험해 본 적이 있긴 하지만, 그저 따라해봤을 뿐이기 때문에..
이미 사용해 본 경험이 있는 MySQL을 활용하여 기존에 알고 있는 쿼리와 연동해서 검색기능을 구현해보고 싶다.
두번째 이유는 도서 검색 기능으로는 FULLTEXT 인덱스로도 충분하다고 판단하였다.
도서 제목, 설명, 키워드와 같은 텍스트 필드를 통해 해당하는 도서 검색 결과만 보여주면 되기 때문이다.
세번째 이유는 도서명을 검색할 때, 키워드 부분 검색도 가능하게 하고 싶었다. LIKE 연산자를 사용하는게 편하지만 FULLTEXT를 선택한 이유이다.
물론, 더 나아가 Elasticsearch와 같은 외부 검색 엔진을 사용하여 비슷한 단어나 연관 단어를 입력해도 원하는 결과가 나오게 업그레이드해보고 싶다.
적용기
인덱스 생성
CREATE FULLTEXT INDEX idx_fulltext ON PRODUCTS (book_name, description);
인덱스 보기
SHOW INDEX FROM PRODUCTS;
전체 텍스트 검색
자연어 검색
특별히 옵션을 지정하지 않거나 뒤에 in natural language mode를 붙이면 자연어 검색을 한다.
자연어 검색은 단어가 정확한 것을 검색해준다.
하지만 검색을 실행했을 때, 정확한 단어를 검색했을 때만 결과값이 나왔다.
SELECT * FROM newspaper WHERE MATCH(article) AGAINST('영화');
SELECT * FROM newspaper WHERE MATCH(article) AGAINST('영화' in natural language mode);
SELECT * FROM newspaper WHERE MATCH(article) AGAINST('영화 배우');
-- ‘영화’ 또는 ‘배우’ 두 단어 중 하나가 포함된 기사 검색.
예를들어, (#문제 1-1)‘영화’라는 정확한 단어만 검색되며 ‘영화는’, ‘영화가’ ..등 능동적인 검색 불가했다.
또한 (#문제 2) 2음절 이하의 단어는 검색이 불가했다.
예를들어, '영화는'은 검색되지만, '영화'는 검색되지 않았다.
불린 모드 검색
(#문제 1-1해결)
이 문제를 해결하기위해 LIKE연산자에서 %를 쓰듯이 불린 모드 검색 기능을 사용한다.
불린 모드 검색은 단어나 문장이 정확히 일치하지 않는 것도 검색하는 것을 의미한다.
뒤에 in boolean mode 옵션을 붙여주면 적용된다.
불린 모드 검색은 필수인 +, 제외하기 위한 -, 부분 검색을 위한 * 연산자 등의 다양한 연산자를 지원한다.
자연어 모드(IN NATURE LANGUAGE MODE)에서 불린 모드(IN BOOLEAN MODE)로 변경하고,
:word 파라미터와 * 와일드카드를 함께 쓸 수 없어서 CONCAT으로 묶어주었다.
@Query(value = "SELECT * FROM PRODUCTS WHERE MATCH(book_name, description) AGAINST(CONCAT(:word, '*') IN BOOLEAN MODE)", nativeQuery = true)
List<ProductEntity> searchByFullText(@Param("word") String word);
불린 모드 검색은 필수인 +, 제외하기 위한 -, 부분 검색을 위한 * 연산자 등의 다양한 연산자를 지원한다.
+ | 검색 필수 SELECT * FROM newspaper WHERE MATCH(article) AGAINST('영화 +액션' IN BOOLEAN MODE); > 영화를 찾되 반드시 액션이 들어가 있는 열 |
- | 검색 제외 SELECT * FROM newspaper WHERE MATCH(article) AGAINST('영화 -액션' IN BOOLEAN MODE); > 영화를 찾되 액션은 안들어가있는 열 |
~ | 검색 부정( - 보다 부드러운 방식 ) SELECT * FROM newspaper WHERE MATCH(article) AGAINST('영화 ~액션' IN BOOLEAN MODE); > ‘영화’를 찾되 ‘액션’이 없는 열보다 ‘액션’이 있는 열이 아래 순위 |
* | 부분 검색 SELECT * FROM newspaper WHERE MATCH(article) AGAINST('영화*' IN BOOLEAN MODE); > ‘영화를’, ‘영화가’, ‘영화는’ 등 |
“ | 부분 검색 “” 안에 있는 구문과 정확히 동일한 철자의 구문 SELECT * FROM newspaper WHERE MATCH(article) AGAINST("재밌는 영화" IN BOOLEAN MODE); > “재밌는 영화”, “재밌는 영화가” 등 > “재밌는 한국 영화”, “재밌는 할리우드 영화” 불가 |
출처: https://inpa.tistory.com/entry/MYSQL-📚-풀텍스트-인덱스Full-Text-Index-사용법#불린_모드_검색 [Inpa Dev 👨💻:티스토리]
문제 1이었던 능동적인 검색은 가능했지만,
하지만 '영화'로 시작하는 접두사여야만 검색이 가능하며, (#문제 1-2) 부분 검색이 되지 않았다.
도서 제목 검색 시, 중간 부분을 기억할 수도 있기 때문에 부분 검색이 필요했다.
부분 일치 검색을 위한 토크나이저 조정
(#문제 1-2해결)
FullText Search에서 인덱스를 생성하는 방법은 여러가지가 있다.
인덱스는 파서가 문자열을 토크나이징한 후에 생성하게 된다.
문장을 키워드로 추출하는 방식을 N-gram 파서로 변경하면 문제를 해결할 수 있다.
N-gram 파서 사용 시, 문장을 연속된 N개의 문자로 분리하여 인덱스를 생성하므로, 부분 일치 검색이 가능해진다.
이로 인해 '영화'와 같은 키워드 검색이 더 유연하게 이루어질 수 있다.
FullText Index 생성 방법
FullText Search에서 인덱스를 생성하는 방법은 여러가지가 있다.
인덱스는 파서가 문자열을 토크나이징한 후에 생성하게 된다. 여기서는 2가지만 알아보겠다.
1. Built-In Parser
Built-In Parser는 stopword(구분자)를 기준으로 키워드를 추출하는 방식이다. 공백이나 문장 기호 혹은 사용자가 지정한 특정 단어를 기준으로 토크나이징하게 된다.
예를 들어, 구분자가 공백이라면 문자이 다음과 같이 쪼개진다.
이세삼은 현재 개발을 공부 중이다. → 이세삼은 / 현재 / 공부 / 중이다
내가 만든 개발프로젝트 → 내가 / 만든 / 개발프로젝트
위와 같은 방식으로 토크나이징 되어 있다면 "세삼은" 혹은 "프로젝트"와 같은 검색 키워드로는 위 문장을 검색할 수 없다.
FullText Search는 토큰과 검색 키워드가 전부 일치하거나 전방(prefix) 일치한 경우에만 결과를 가져오기 때문이다.
2. N-gram Parser
위와 같은 문제를 해결해주는 파서도 존재한다.
N-gram Parser는 MySQL에서 기본적으로 제공하기 때문에 FullText Index를 설정해줄 때 옵션으로 지정해주기만 하면 사용할 수 있다.
기존에 생성한 FullText Index를 삭제 후, N-gram Parser 옵션(WITH PARSER ngram)을 추가 한 뒤, 다시 생성해 주었다.
FullText Index 삭제하기
drop index idx_fulltext on PRODUCTS;
N-gram Parser 옵션(WITH PARSER ngram) 추가
CREATE FULLTEXT INDEX idx_fulltext ON PRODUCTS (book_name, description) WITH PARSER ngram;
N-gram Parser는 지정된 토큰 사이즈를 기준으로 키워드를 추출한다.
예를 들어, 토큰 사이즈가 2라면 아래와 같이 쪼개진다.
이세삼은 현재 개발을 공부 중이다. → 이세 / 삼은 / 현재 / 개발 / 을 공 / 부 중 / 이다.
내가 만든 개발프로젝트 → 내가 / 만든 개발 / 프로 / 젝트
- 하지만, N-gram Parser는 공백이 포함된 경우 키워드로 추출하지 않기 때문에 "을 공" 또는 "부 중"을 검색했을 때 색이 되지 않았다.
하지만, (#문제 1-2 해결) "세삼"을 검색했을 때는 부분 문자열 검색 또는 부분 일치 기능 때문에 결과가 올바르게 나왔다.
💡 N-그램이란?
**N-그램(N-gram)**은 텍스트에서 연속된 N개의 항목(보통 단어 또는 문자)을 하나의 그룹으로 묶는 것을 말합니다.
여기서 N은 묶는 항목의 수를 의미합니다.
💡 2-그램 (바이그램)
2-그램(또는 바이그램)은 N-그램에서 N이 2인 경우를 가리킵니다.
즉, 텍스트를 두 개의 연속된 단어 또는 문자로 나누는 것입니다.
💡 토큰 사이즈와 N-그램 파서의 이해
- N-그램 파서는 텍스트를 연속된 N개의 문자의 그룹으로 나누는 방법입니다. 예를 들어, N이 2인 경우, 텍스트는 2-그램으로 나뉩니다.
- 토큰 사이즈는 N-그램 파서가 사용하는 N의 값입니다. 따라서, 토큰 사이즈가 2라면 2-그램을 사용하여 텍스트를 두 개의 연속된 문자로 나누는 것입니다.
2-gram Parser일 경우, token size는 2이다.
N-Gram 검색 vs. 불리언 모드 접두사 검색: 어느 것이 더 효과적일까?
현재 WITH PARSER ngram
과 불리언 모드 접두사 검색(:word*)
둘 다 적용 되어 있는데, 방식 모두 부분 문자열 검색 기능을 제공하므로 검색 기능에서 중복이 발생하는 것으로 보였다.
이 두 검색 방식이 중복되기 때문에, 함께 사용할 필요가 있는지 의문이 생겼다.
N-Gram 검색: 인덱스 생성
CREATE FULLTEXT INDEX idx_fulltext ON PRODUCTS (book_name, description) WITH PARSER ngram;
불리언 모드 접두사 검색(*): 쿼리 실행
@Query(value = "SELECT * FROM PRODUCTS WHERE MATCH(book_name, description) AGAINST(CONCAT(:word, '*') IN BOOLEAN MODE)", nativeQuery = true)
List<ProductEntity> searchByFullText(@Param("word") String word);
역시나 검색이라는 부분에서는 중복이 맞았다.
부분 문자열 검색 기능이라는 공통점도 있지만, 검색 방식에서 차이가 있다.
검색 방식의 차이
- N-Gram 검색 (WITH PARSER ngram): 부분 문자열 검색에 중점을 두며, 텍스트를 n-그램으로 나누어 인덱스를 생성한다. 이 방법은 텍스트의 부분 문자열을 효과적으로 검색할 수 있다.
- 불리언 모드 접두사 검색 (word*): 입력한 단어로 시작하는 모든 단어를 찾는다. 특정 패턴이나 접두사로 시작하는 단어를 검색하는 데 유리하다.
결론
N-Gram 검색만을 사용하기로 결정했다.
이유
- 범위: N-Gram 검색은 검색 범위가 넓어 :word*와 같은 접두사 기반 검색을 별도로 사용할 필요가 없다. n-그램 검색만으로도 충분히 많은 검색 결과를 제공하기 때문이다.
- 독립적 작동: 불리언 모드 접두사 검색은 N-Gram 설정과 독립적으로 작동하기 때문에, N-Gram 인덱스와 불리언 모드 검색은 각각 독립적으로 처리되므로, 두 검색 방식을 병행할 필요가 없다.
- 단순화: 검색 방식을 단순화하면 시스템 유지 관리와 성능 조정이 더 쉬워지기 때문에, 하나의 검색 방법을 명확히 정의하고 사용하는 것이 더 효율적이다.
(#문제 2)인 2음절 이하의 단어는 여전히 검색이 불가했다.
검색 단어 제한 수 풀기
(#문제 2 해결)
mysql은 기본값으로 검색 가능 단어의 숫자는 3이다.
즉, 3글자 이상만 전체 텍스트 검색이 된다.
따라서 검색 가능 단어의 숫자를 확인하고 mysql 설정에서 수정해줘야 한다.
-- 몇 글자 이상부터 검색 가능한지 확인
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';
docker에 mysql을 올려놨기 때문에 my.cnf에 들어가서 innodb_ft_min_token_size=2 을 추가해주면 된다.
MySQL설정 파일 수정
방법 #1 docker 접속
1. 컨테이너 접속
docker exec -it [컨테이너 이름] bash
2. MySQL 설정 파일 찾기
cd /etc/mysql/my.cnf
cat /etc/mysql/my.cnf
3. 파일 편집
my.cnf 파일을 수정하여 innodb_ft_min_token_size 값을 변경
vi /etc/my.cnf
>> 😥 험난기..
vi /etc/my.cnf 편집이 안되어서
맨 마지막 줄에 한 줄 추가하는 방법을 사용해보았다.
echo "innodb_ft_min_token_size=2" | sudo tee -a /etc/my.cnf > /dev/null
하지만 설정 적용 순서 에러가 났다.
[mysqld] 섹션에 innodb_ft_min_token_size=2 이 들어가야 하는데,
맨 마지막에 있는 [client]에 들어가게 되어 설정 적용 순서 에러가 난다.
그래서 직접 파일에 들어가서 설정을 추가해주었다.
방법 #2 직접 파일에서 수정
docker → mysql → Files → my.cnf 로 직접 들어가서 innodb_ft_min_token_size=2 추가 해주었다.
[client]가 아닌 [mysqld] 안에 추가 해주어야 한다.
재시작 해주면 된다.
innodb_ft_min_token_size = 2로 바뀐 것을 확인할 수 있다.
참고 링크
[MYSQL] 📚 풀텍스트 인덱스(Full-Text Index) 사용법
Full Text Search를 이용한 DB 성능 개선 일지
'Project > Collabo Project' 카테고리의 다른 글
[Villion] eureka 배포 (0) | 2024.08.29 |
---|---|
[Villion] Docker+GCP로 프론트 배포 정리 (1) | 2024.08.28 |
[Villion] 이 책과 함께 구매한 도서 목록 구현하기 (0) | 2024.08.14 |
[Villion] JPA 양방향에서 단방향으로 변경 (0) | 2024.07.26 |
[Villion] 동시성 문제 해결 방안 (2) | 2024.07.19 |