Backend

[Backend] MySQL에서 한글 검색을...?

아몬드맛빼빼로 2025. 2. 6. 13:35
반응형

 

문제상황

여느때와 같이 몇몇의 친구들과 토이 프로젝트를 하고 있던 어느 날 심각한 고민에 빠지게 된다.

한글로 저장된 사용자들의 이름을 검색할 수 있어야 하는데 지금까지는 MongoDB,Elaticsearch의 Nori 형태소 분석기를 써서 아주 자연스러운 한글 검색을 구현해냈지만 많아봐야 150여개의 적은 데이터에서 길어봤자 10자를 안넘어가는 한글 검색을 위해 하나의 DB를 추가적으로 운용하자는 것은 오히려 득보다 실이 더 많을 것 같았다.

그냥 Like문이나 쓰면 안되나?

거두절미하고 사실 이미 간단한 검색은 충분이 구현 가능하다.

SELECT * 
FROM member 
WHERE name LIKE '%김%';

다음과 같은 쿼리문을 작성할 수 있다면 "member 테이블에서 -> name 칼럼의 -> "김"이 포함된 모든 데이터"를 조회할 수 있다.

또한, '%' 기호를 이용하여 '%김'의 경우는 '김'으로 끝나는 데이터를 조회하거나 반대의 경우에서도 '김'으로 시작하는 데이터를 조회할 수도 있다.

한계점

Like 문을 이용하는 것은 한계점이 아주 명확하다.지금과 같이 '이름'을 검색하는 등의 상황에서는 결과물은 보장될 수 있지만 만약 검색하려는 데이터가 아주 긴 "문장"이거나 데이터가 아주 많다면? 쿼리 1회에 n초씩 걸리는 파멸적인 성능을 가지게 될 것이다.

FullText Search

 본래 한글은 끝맺음이 분명하지 않다.

나는 GSM에 입학했다

다음과 같은 문장에서 "나는"이라는 단어는 "나"나는 대명사와 "는"이라는 주격조사로 이뤄져 있다.MySQL 5.6까지는 이러한 한글에 대응하려면 DB에 INSERT 전에 문장,단어를 전처리해야 그나마 미끄러운 검색이 가능했다.


MySQL 5.7이 출시되며 변환점을 맞게 되었는데 Parser로 한국어/일본어에 대응되는 N-gram이 탑재되었다.이전에도 1byte 언어의 FTS(FullText Search)는 지원되었으나 한국어,일본어와 같이 1글자가 2byte로 구성된 언어가 온전히 지원되기 시작한 것이다.

사용 전 주의점

FTS를 사용하기 전 주의해야 할 사항이 있는데 바로 'innodb_ft_result_cache_limit' 설정값이다.Thread 당 InnoDB FTS결과에 대한 Cache의 제한을 설정하는 것으로 InnoDB의 FTS 처리는 Memory에서 이뤄지기 때문에 해당 설정이 FTS로 인하여 지나치게 Memory가 사용되는 것을 막는것이다.최대 설정값은 4GB이다.

사용법

먼저 MySQL 5.7 이상 버전을 설치한다.

sudo apt install mysql-server

여기까지만 하고 바로 테이블을 알맞게 생성해서 FTS를 사용하려고 해도 치명적인 문제는 없지만 한글은 2byte 문자기 때문에 기본 설정인 InnoDB + MyISAM 엔진 설정은 한글을 제대로 지원하지 못한다.


앞서 언급되었던 N-gram을 설정해줘야 하는데 먼저 MySQL 설정 파일을 조작해주어야 한다

sudo nano /etc/mysql/my.cnf

이 파일에서 [mysqld] 항목에 다음과 같이 추가한다

[mysqld]
innodb_ft_min_token_size=2
ft_min_word_len=2

여기서 'innodb_ft_min_token_size'는 InnoDB 엔진에서 최소 토큰 크기를 한글에 맞게 2byte로 변경하는 것이고,

'ft_min_word_len' 역시 MyISAM에서 최소 단어 길이를 2로 변경하는 것이다.

저장 후 MySQL을 재시작 해주면 된다.

sudo systemctl restart mysql

이후 DB 콘솔에서 N-gram Parser를 활성화 해줘야 하는데

SET GLOBAL innodb_ft_server_stopword_table = '';
ALTER TABLE articles ADD FULLTEXT INDEX fts_index (content) WITH PARSER ngram;

여기서는 "articles 테이블에서 fts_index 라는 이름의 인덱스를 content 칼럼을 이용해서 생성하겠다"라는 내용으로 'articles','fts_index','content'를 각각 상황에 맞게 적용하면 된다.


실제 데이터를 위한 테이블을 생성해야 하는데 글을 저장하는 'articles' 테이블을 생성한다고 가정한다.

CREATE TABLE articles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,
    FULLTEXT(content) WITH PARSER ngram
) ENGINE=InnoDB;

여기서 'content' 칼럼에 FULLTEXT 를 적용하며 Parser로 ngram을 적용해준다.


임의의 데이터를 넣고 실제 쿼리문은

SELECT * FROM articles WHERE MATCH(content) AGAINST ('한글' IN NATURAL LANGUAGE MODE);

다음과 같이 작성할 수 있는데 'MATCH(content) AGAINST ('한글')' 은 content 칼럼에서 '한글'이 포함된 데이터를 조회하는며  IN NATURAL LANGUAGE MODE 설정은 자연어 검색 모드로 쿼리한다는 의미이다.

더보기

NATURAL LANGUAGE MODE를 사용하면 FTS가 자동으로 연관성 높은 단어를 추출하여 조회를 수행한다

 

다른 모드로  BOOLEAN MODE도 있는데 

SELECT * FROM articles WHERE MATCH(content) AGAINST ('+한글 -검색' IN BOOLEAN MODE);

다음과 같은 쿼리문을 작성하면 '한글'이 반드시 포함되며 '검색'이 반드시 미호함되어 있는 데이터를 조회할 수 있다.이름 그대로 Boolean 연산자를 이용할 수 있다는 의미이다.

연산자 의미
+ 해당 단어가 반드시 포함되어야 함
- 해당 단어가 포함되지 않아야 함
* 해당 단어의 접두어까지 검색('한글'을 검색하면 '한글화'등도 검색)
"" 정확히 해당 구문을 포함해야 함
~ 해당 단어가 포함된 결과의 점수(가중치)를 낮춤
>< 연관성 가중치 조정

 

또다시 한계점

한계점을 단적으로 보여주는 예시이다.적은 양의 데이터에서는 크게 차이가 나지 않지만 데이터의 규모가 커질수록 Inverted Index,즉 역색인 방식의 검색엔진(Elasticsearch 등)들과 차이가 커진다. 20만건 이상의 데이터를 조회할 때 MySQL은 1분 30초 이상의 쿼리 시간을 가져갔지만 Elasticsearch는 530ms의 쿼리 시간만이 필요했다.이처럼 대규모 데이터 조회에선 극명한 한계점을 보인다.(출처)

결론

대규모 데이터에선 한계를 보이지만 MySQL과 같은 보편적인 RDB에서도 충분한 대안을 제시하고 있으니 한글 검색이나 전문 검색을 구현할때 적당한 데이터 규모가 예측된다면 고려해볼만한 선택지가 될것 같다.

'Backend' 카테고리의 다른 글

[Backend] Grafana Loki  (0) 2025.03.06
[Backend] gRPC  (1) 2025.02.28
[Backend] Elasticsearch  (0) 2024.12.23
[Backend] Redisson Pub/Sub 기반 분산 락  (0) 2024.11.30
[Backend] GraphQL  (0) 2024.11.25