MySQL Ngram, 제대로 이해하기

2022. 4. 25. 23:56BACKEND/Database

반응형

MySQL의 fulltext 검색 알고리즘 중, ngram을 이해하고 사용할 줄 알게끔 습득하는 것이 해당 포스팅의 목표입니다.

 

 

 

👜 Ngram?

이전 포스팅에서 Full-Text 검색을 다루었는데요.

이번에는 단어을 파싱하여 검색하는 과정에서 필요한 parser 중 "ngram"에 대해 포스팅하고자 합니다.

Ngram의 특징에는 아래 3가지가 있습니다.

 

✔️  Built-in

ngram은 MySQL의 Built-in Parser로써, 다른 기본 제공 서버 플러그인과 마찬가지로 서버가 시작될 때 자동으로 로드됩니다.

즉, 따로 설치할 필요없이 사용할 수 있다는 큰 장점을 가집니다.

 

✔️  MySQL의 지원

또한 InnoDBMyISAM 엔진을 지원하며, ngram은 중국어, 일본어, 한국어(CJK)를 지원합니다.

 

✔️ 버전 체크 

Full Text Search 적용은 MyISAM은 MySQL 5.5 버전 이상부터, innoDB는 MySQL 5.6 버전 부터 지원합니다

 

 

 

참고로, 한글 형태소 분석을 위한 은전한닢 MeCab도 있습니다.

하지만 MeCab을 위한 형태소 분석은 매우 전문적인 검색 알고리즘이라서,

만족할만한 검색 결과를 내기 위해 많은 시간이 걸린다고 합니다.

전문적인 검색 엔진이 아닌, 단순한 키워드 검색을 위한다면 인덱싱 알고리즘인 N-gram을 사용할 수 있습니다.

추가로, MeCab을 사용하기 위해서는 따로 설치 후 플러그인 연결 설정이 필요합니다.

 

 

 

Tokenize

ngram 파서는 일련의 텍스트를 n개의 문자로 구성된 연속된 시퀀스로 토큰화합니다.

예를 들어, n그램 전체 텍스트 파서를 사용하여 n의 다른 값에 대해 "abcd"를 토큰화할 수 있습니다.

 

n=1: 'a', 'b', 'c', 'd'
n=2: 'ab', 'bc', 'cd'
n=3: 'abc', 'bcd'
n=4: 'abcd'

 

한글에서는 어떻게 토큰화할까요?

가령 토큰 사이즈가 2라고 할 때 "철학은 어떻게 삶의 무기가 되는가"라는 책 제목을 검색한다면,

 

["철학", "학은", "어떻", "떻게", "삶의", "무기", "기가", "되는", "는가"]

와 같이 공백이 무시된 채로 토큰화합니다.

 

그렇다면, 책 제목을 검색할 때 "철학", "삶의" 라는 키워드로 검색할 때 검색 결과에 걸리게 되는 것이죠.

 

실제로 간단한 테스트를 해보겠습니다.

 

CREATE TABLE IF NOT EXISTS `books` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(100) NOT NULL,
  `author` VARCHAR(350) NULL DEFAULT NULL,
  `reg_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`), 
  FULLTEXT INDEX `ft_idx_title` (`title`)  WITH PARSER `ngram`
);

 

test DB에 위와 같이 아주 간단한 books 테이블을 정의할 수 있습니다.

아래에서 다루겠지만 간단히 설명하면,

"title" 이라는 컬럼에 ngram parser를 사용하는 FullText Index를 건 채로 테이블을 생성했습니다.

 

이제, 아래와 같이 데이터를 삽입해보겠습니다.

 

mysql> INSERT INTO books(title, author) VALUES('철학은 어떻게 삶의 무기가 되는가','야마구치 슈');

mysql> SELECT * FROM books; 
+----+------------------------------------------------+------------------+---------------------+ 
| id | title                                          | author           | reg_at              | 
+----+------------------------------------------------+------------------+---------------------+ 
| 1  | 철학은 어떻게 삶의 무기가 되는가                       | 야마구치 슈         | 2022-04-25 23:38:07 |
+----+------------------------------------------------+------------------+---------------------+

 

그리고는 아래와 같이 확인할 수 있습니다.

 

mysql> set global innodb_ft_aux_table = 'test/books';
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE;
+--------+--------------+-------------+-----------+--------+----------+
| WORD   | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+--------+--------------+-------------+-----------+--------+----------+
| 기가    |            2 |           2 |         1 |      2 |       30 |
| 는가    |            2 |           2 |         1 |      2 |       40 |
| 되는    |            2 |           2 |         1 |      2 |       37 |
| 떻게    |            2 |           2 |         1 |      2 |       13 |
| 무기    |            2 |           2 |         1 |      2 |       27 |
| 삶의    |            2 |           2 |         1 |      2 |       20 |
| 어떻    |            2 |           2 |         1 |      2 |       10 |
| 철학    |            2 |           2 |         1 |      2 |        0 |
| 학은    |            2 |           2 |         1 |      2 |        3 |
+--------+--------------+-------------+-----------+--------+----------+
9 rows in set (0.00 sec)

 

테스트를 직접 해보니까 재밌네요 ㅎㅎ

독자분들도 직접 해보시는 걸 추천드립니다 🙌🏻

 

그럼 이제부터 어떻게 사용할지, 사용법을 소개하도록 하겠습니다.

 

 

🎟  Token Size

먼저, Ngram을 사용할 때의 토큰 사이즈를 지정해야겠죠?

ngram 파서의 기본 ngram 토큰 크기는 2(bigram)입니다.

 

 

Show Token Size

확인을 하고 싶다면, Global Variables 확인 구문을 통해 확인할 수 있습니다.

 

mysql> SHOW GLOBAL VARIABLES LIKE "ngram_token_size";
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| ngram_token_size | 2     |
+------------------+-------+
1 row in set (0.01 sec)

 

 

ngram 토큰 크기는 ngram_token_size  옵션을 사용하여 구성할 수 있습니다.

이 옵션은 최소 값이 1이고 최대 값이 10입니다.

 

일반적으로 ngram_token_size는 검색하려는 가장 큰 토큰 크기로 설정하면 됩니다.

가령, 한 글자(문자)만 검색하려면 ngram_token_size를 1로 설정하면 됩니다.

이때, 토큰 크기가 작을수록 전체 텍스트 검색 색인이 작아지고 검색 속도가 빨라집니다.

 

 

Set Token Size

ngram_token_size는 mysqld 실행 시 option으로 설정(startup string)하거나 my.conf 구성 파일에서 설정할 수 있습니다.

 

mysqld --ngram_token_size=2

# OR

[mysqld]
ngram_token_size=2

 

 

ngram_token_size

이 때, ngram 파서를 사용하는 FULLTEXT 인덱스의 경우 innodb_ft_min_token_size, innodb_ft_max_token_size, ft_min_word_len, ft_max_word_len 의 구성 옵션이 무시됩니다.

대신 토큰을 제어하기 위해 ngram_token_size 을 사용하게 됩니다

 

 

 

💎 Creating Index

ngram 파서를 사용하는 FullText Index를 생성하는 방법으로 CREATE TABLEALTER TABLE, 이나 CREATE INDEX 구문을 사용할 수 있습니다.

방식은 아래의 예시를 통해 확인할 수 있습니다.

 

# 1. CREATE TABLE ~
CREATE TABLE articles (
      id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
      title VARCHAR(200),
      body TEXT,
      FULLTEXT (title,body) WITH PARSER ngram
) ENGINE=InnoDB CHARACTER SET utf8mb4;

# 2. ALTER TABLE ~
ALTER TABLE articles ADD FULLTEXT INDEX ft_index (title,body) WITH PARSER ngram;

# 3. CREATE .. INDEX ~
CREATE FULLTEXT INDEX ft_index ON articles (title,body) WITH PARSER ngram;

 

 

🔍 Search 

사용하는 방식은 기본 FullText 검색과 동일합니다.

 

# MATCH() in SELECT list...
SELECT MATCH (a) AGAINST ('abc') FROM t GROUP BY a WITH ROLLUP;
SELECT 1 FROM t GROUP BY a, MATCH (a) AGAINST ('abc') WITH ROLLUP;

# ...in HAVING clause...
SELECT 1 FROM t GROUP BY a WITH ROLLUP HAVING MATCH (a) AGAINST ('abc');

# ...and in ORDER BY clause
SELECT 1 FROM t GROUP BY a WITH ROLLUP ORDER BY MATCH (a) AGAINST ('abc');

 

ngram을 통한 검색 시에 여러가지 고려할 점이 있는데요.

같이 살펴보도록 하겠습니다.

 

 

 

Stopword Handling

n-gram에서는 스탑워드(stopwords) 처리가 조금 다릅니다.

 

일반적으로 토큰화된 단어 자체(완전히 일치)가 stopwords 테이블에 있다면 그 단어는 전문 검색 인덱스에 추가되지 않습니다. 

그러나 n-gram 파서의 경우, 토큰화된 단어에 stopwords가 포함되어 있는지 확인하고 포함된 경우엔 토큰을 제외합니다.

 

예를 들어 ngram_token_size = 2라고 가정하면 "a,b"가 포함 된 문서는 "a," 와 ",b" 로 구문 분석됩니다.

이 때 쉼표 (",")가 stopwords 로 정의 된 경우,

n-gram을 사용하지 않았다면 ",(쉼표)"와 완전히 동일한 경우이어야 제외가 되었겠지만,

n-gram을 사용한다면 "a," 와 ",b" 는 모두 쉼표를 포함하므로 검색에서 제외됩니다.

 

 

 

Space Handling

n-gram에서 공백은 항상 제외 됩니다.

Stopword의 공백이 항상 포함되어 있다고 생각하면 조금 더 이해가 잘 되실텐데요.

 

예를 들어 "ab cd" 는 "ab" , "cd" 로 구문 분석되며, "a bc" 는 "bc" 로 구문 분석이 됩니다.

 

한글의 경우도 동일합니다. 위의 books 테이블로 예시를 들자면,

"나 자신을 알라"라는 책 제목을 파싱하면 어떻게 될까요?

 

책 제목이 "나 자신을 알라"이고 저자가 "스티븐 M. 플레밍"인 데이터를 넣고 확인해보면 아래와 같습니다.

 

mysql> INSERT INTO books(title, author) VALUES('나 자신을 알라', '스티븐 M. 플레밍');

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE;
+--------+--------------+-------------+-----------+--------+----------+
| WORD   | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+--------+--------------+-------------+-----------+--------+----------+
| 신을    |            4 |           4 |         1 |      4 |        7 |
| 알라    |            4 |           4 |         1 |      4 |       14 |
| 자신    |            4 |           4 |         1 |      4 |        4 |
+--------+--------------+-------------+-----------+--------+----------+

 

"나 (공백)" 은 포함되지 않는 것을 확인할 수 있습니다.

 

 

 

 

Term Search

자연어 모드 검색(natural language mode) 의 경우 용어들의 조합(union) 검색으로 변환되고,

불린 모드 검색(boolean mode)의 경우 구문(phrase) 검색으로 변환됩니다. 

검색 모드는 이전 포스팅을 참고해 주세요.

 

예를 들어 ngram_token_size가 2일 때, 문자열 "abc"는 "ab bc"로 변환되고,

"ab"을 포함하는 문서와 "abc"를 포함하는 문서 두 개가 주어졌다고 가정해봅시다.

자연어 모드 검색(natural language mode) 의 경우 검색어 "ab bc"는 두 문서와 일치합니다.

부울 모드 검색(boolean mode)의 경우, 오직 'abc'를 포함하는 문서만 일치합니다.

 

한글로 예를 들면, "철학을" 이라는 검색어가 있다고 해봅시다.

그럼 ["철학", "학을"] 로 파싱이 되어 검색을 진행합니다.

 

자연어 모드에서는 "철학", "학을" 용어 중 일치하는 게 있으면 출력하고,

불린 모드에서는 "+철학 +학을"로 검색됩니다.

 

mysql> SELECT * FROM books WHERE MATCH(title) AGAINST("철학을");
+----+------------------------------------------------+------------------+---------------------+
| id | title                                          | author           | reg_at              |
+----+------------------------------------------------+------------------+---------------------+
|  1 | 철학은 어떻게 삶의 무기가 되는가                       | 야마구치 슈         | 2022-04-25 23:38:07 |
+----+------------------------------------------------+------------------+---------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM books WHERE MATCH(title) AGAINST("철학을" IN BOOLEAN MODE);
Empty set (0.00 sec)

 

 

 

Wildcard Search

와일드카드 검색의 접두사 용어가 ngram 토큰 크기보다 짧으면,

쿼리는 접두사 용어로 시작하는 ngram 토큰을 포함하는 모든 인덱스 행을 반환합니다. 
예를 들어, ngram_class_size=2라고 가정하면 "a*"에 대한 검색은 "a"로 시작하는 모든 행을 반환합니다.

 

하지만, 와일드카드 검색의 접두사 용어가 ngram 토큰 크기보다 길면

접두사 용어가 ngram 구문으로 변환되고 와일드카드 연산자는 무시됩니다. 
예를 들어 ngram_control_size=2라고 가정하면 "abc*" 와일드카드 검색은 "ab bc"로 변환됩니다.

 

다른 예로는, "sq*"는 "sq"로 변환되어 "sql*" 는 "sq ql" 로 변환됩니다.

 

mysql> SELECT title FROM books WHERE MATCH(title) AGAINST ('SQ*' IN BOOLEAN MODE);
+--------+
| title  |
+--------+
| MY SQL |
| MYSQL  |
| SQL    |
+--------+

mysql> SELECT title FROM books WHERE MATCH(title) AGAINST ('SQL*' IN BOOLEAN MODE);
+--------+
| title  |
+--------+
| MY SQL |
| MYSQL  |
+--------+

 

첫 번째의 경우에는 2인 token size보다 접두사가 짧기 때문에 "SQ"로 시작하는 단어들을 모두 반환한 것을 확인할 수 있습니다.

반면 두번째는, 접두사가 더 길기 때문에 "+SQ +QL"로 검색이 되어 SQL이 검색에서 빠진 것을 확인할 수 있습니다.

 

 

 

Phrase Search

구문 검색은 n그램 구문 검색으로 변환됩니다. 

예를 들어, 검색어 "abc"는 "abc"와 "ab bc"를 포함하는 문서를 반환하는 "ab bc"로 변환됩니다.

검색어 "abc def"는 "abc def"와 "ab bc de ef"를 포함하는 문서를 반환하는 "ab bc de ef"로 변환됩니다. 
"abcdef"가 포함된 문서는 반환되지 않습니다.

 

 

 

 

 

 

그럼 지금까지 FullText 검색의 ngram에 대한 내용을 살펴보았습니다.

오타나 잘못된 내용을 댓글로 남겨주세요!

감사합니다 ☺️ 



반응형

Backend Software Engineer

Gyeongsun Park