MySQL FullText Search

김기욱·2020년 12월 28일
3

전문검색

인덱스 알고리즘은 일반적으로 크지 않은 데이터 또는 이미 키워드화돼 있는 작은 값에 대한 인덱싱 알고리즘이었습니다. 대표적으로 MySQL의 B-Tree 인덱스는 실제 컬럼의 값이 1MB라 하더라도 1MB 전체의 값을 인덱스 키로 사용하는 것이 아니라 1,000바이트(MyISAM) 또는 767바이트(InnoDB)까지만 잘라서 인덱스 키로 사용합니다. 또한 B-Tree 인덱스의 특성에서도 알아봤듯이 전체 일치 또는 좌측 일부 일치와 같은 검색만 가능합니다.

문서의 내용 전체를 인덱스화해서 특정 키워드가 포함된 문서를 검색하는 전문(Full Text) 검색에는 InnoDB나 MyISAM 스토리지 엔진에서 제공하는 일반적인 용도의 B-Tree 인덱스를 사용할 수 없습니다. 문서 전체에 대한 분석과 검색을 위한 이러한 인덱싱 알고리즘을 전문 검색(Full Text search) 인덱스라고 하는데, 전문 검색 인덱스는 일반화된 기능의 명칭이지 전문 검색 알고리즘의 이름을 지칭하는 것은 아닙니다.

전문 검색 인덱스에는 문서의 키워드를 인덱싱하는 기법에 따라 크게 구분자(Stopword)와 N-그램으로 나눠서 생각해 볼 수 있습니다. 이 이외의 알고리즘은 그다지 알려지지도 않고 특히나 MySQL에서 사용할 수 있는 것이 없습니다.

인덱스 알고리즘

전문 검색에서는 문서 본문의 내용에서 사용자가 검색하게 될 키워드를 분석해 내고, 빠른 검색용으로 사용할 수 있게 이러한 키워드로 인덱스를 구축합니다. 키워드의 분석 및 인덱스 구축에는 여러 가지 방법이 있을 수 있습니다. 여기서는 MySQL 모든 버전에서 기본적으로 제공하는 전문 검색 엔진의 인덱스 방식인 구분자(Stopword)와 MySQL 5.7부터 InnoDB Storage에 추가된 기능인 N-그램 방식에 대해 살펴보겠습니다.

구분자(Stopword) 기법

전문의 내용을 공백이나 탭(띄어쓰기) 또는 마침표와 같은 문장 기호, 그리고 사용자가 정의한 문자열을 구분자로 등록합니다. 구분자 기법은 이처럼 등록된 구분자를 이용해 키워드를 분석해 내고, 결과 단어를 인덱스로 생성해 두고 검색에 이용하는 방법을 말합니다. 일반적으로 공백이나 쉼표 또는 한국어의 조사 등을 구분자로 많이 사용합니다. 기존에 MySQL의 내장 전문 검색(FullText search) 엔진은 구분자 방식만으로 인덱싱할 수 있습니다.

구분자 기법은 문서의 본문으로부터 키워드를 추출해 내는 작업이 추가로 필요할 뿐, 내부적으로는 B-Tree 인덱스를 그대로 사용합니다. 전문 검색 인덱스의 많은 부분은 B-Tree의 특성을 따르지만 전문 검색 엔진을 통해 조회되는 레코드는 검색어나 본문 내용으로 정렬되어 조회되지는 않습니다. 전문 검색에서 결과의 정렬을 일치율(Match percent)이 높은 순으로 출력되는 것이 일반적입니다.

구분자 기법으로 전문 검색을 사용할 때는 문장 기호뿐 아니라 특정 단어를 일부러 구분자로 등록할 수도 있습니다. 예를 들어 MySQL 매뉴얼을 페이지 단위로 잘라서 테이블에 저장하고 전문 검색을 구현한다고 해봅시다. 이 경우 테이블의 모든 레코드에는 "MySQL"이라는 단어가 포함돼 있을 것입니다. 이 경우 "MySQL"이라는 단어로 검색하면 테이블의 모든 레코드가 일치하므로 검색의 효과가 없어집니다. 이럴 때는 "MySQL"이라는 단어를 구분자에 등록하고 전문 검색 인덱스에 포함하지 않게 해주는 것이 좋습니다.

많은 인터넷 사이트에서 "Stopword"를 "불용어"로 해석하고 있지만, 이보다는 "구분자"라는 표현이 더 적절한 해석이라고 볼 수 있습니다. 이 기법의 알고리즘에서 "Stopword"는 "검색에 사용할 수 없다"보다는 "검색어를 구분해주는 기준(문장 기호나 특정 문자열)이다"의 의미가 더 강하기 때문입니다.

N-그램(N-Gram)기법

하지만 각 국가의 언어는 띄어쓰기가 전혀 없다거나 문장 기호가 전혀 다른 경우가 허다합니다. 이런 다양한 언어에 대해 하나의 규칙을 적용해 키워드를 추출해내기란 쉽지 않습니다. 또한 구분자 방식은 추출된 키워드의 일부(키워드의 뒷부분)만 검색하는 것은 불가능하다는 단점도 있습니다. 이러한 부분을 보완하기 위해 지정된 규칙이 없는 전문도 분석 및 검색을 가능하게 하는 방법이 N-그램이라는 방식입니다.

N-그램이란 본문을 무조건적으로 몇 글자씩 잘라서 인덱싱하는 방법입니다. 구분자에 의한 방법보다는 인덱싱 알고리즘이 복잡하고, 만들어진 인덱스의 크기도 상당히 큰 편입니다. 트리톤(Tritonn)이나 스핑크스(Sphinx)에서는 다른 인덱싱 방법도 제공하지만, 이 알고리즘이 주로 사용됩니다. N-그램에서 n은 인덱싱할 키워드의 최소 글자(또는 바이트) 수를 의미하는데, 일반적으로는 2글자 단위로 키워드를 쪼개서 인덱싱하는 2-Gram(또는 Bi-Gram이라고도 한다) 방식이 많이 사용됩니다.

실제로 MySQL의 기본 ngram_token_size도 2(Bi-Gram)입니다.

참고로 ngram_token_size은 MySQL 기본 FullText Search Index에 적용되어 있는 innodb_ft_max_token_size & innodb_ft_min_token_size와 다릅니다. 후자의 경우 FTSI에 쓸 수 있는 단어의 길이를 제한하는 것으로 3글자 이상부터 84글자 이하까지 인덱스 단어로써 사용이 가능하다는 의미입니다.

추가사항 : innodb_ft_min_token_size은 MYSQL 버전4 이후로 byte가 아닌 char이 기준입니다.

그러므로 '지갑'같은 두 글자 검색은 일반적인 전문검색으로는 검색되어지지않습니다. byte기준으론 3을 넘어가나 char으로는 'ap'와 같은 알파벳 두 글자와 마찬가지로 char이 2기 때문입니다.
하지만 ngram_parser방식을 쓰신다면 이를 신경쓰지 않으셔도 됩니다. ngram_parser는 innodb_ft_min_token_size와는 별개로 동작하기 때문입니다.

2-Gram 인덱싱 기법은 2글자 단위의 최소 키워드에 대한 키를 관리하는 프론트엔드(Front-end) 인덱스와 2글자 이상의 키워드 묶음(n-SubSequence Window)를 관리하는 백엔드(Back-end) 인덱스 2개로 구성됩니다. 인덱스의 생성 과정은 다음과 같이 2가지 단계로 나눠서 처리됩니다.

첫 번째 단계로, 문서의 본문을 2글자보다 큰 크기로 블록을 구분해서 백엔드 인덱스(3)을 생성
두 번째 단계로, 백엔드 인덱스의 키워드들을 2글자씩 잘라서 프론트엔드 인덱스(6)을 생성

인덱스의 검색 과정은 전문 인덱스의 생성과는 반대로, 입력된 검색어를 2바이트 단위로 동일하게 잘라서 프론트엔드 인덱스를 검색합니다. 그 결과를 대상 후보 군으로 선정하고, 백엔드 인덱스를 통해 최종 검증을 거쳐 일치하는 결과를 가져옵니다.

예를들어 만약 '개발자기욱' 을 2-Gram Parser를 써서 검색한다면
'개발' '발자' '자기' '기욱' 2글자씩 쪼개서 검색하게 됩니다.

Ngram Parser 사용하기

MySQL Built-in parser는 StopWord parser지만 개인적으로는 검색의 정확도를 위해선 Ngram parser를 사용하기를 추천합니다. StopWord parser는 다음과 같은 치명적인 단점이 존재하기 때문입니다.

아래와 같은 데이터가 포함된 테이블을 예로 살펴보겠습니다.

Table : Hiring

sample_idsample_content
1주방보조 : 6개월 성실하고 오래 근무할 군필남자 직원 구합니다
2테이블담당 : 업계 최고대우! 최소 1년이상 일하실 L호텔 테이블 담당 군필남자분 구합니다!
3단기알바 : 쿠팡 물류센터 상하차 담당 군필남자 일일 알바 구합니다!(장기가능)

FTSI(Fulltext SearchIndex)를 생성한 후 다음과 같은 쿼리를 실행하면 결과는 어떻게 될 까요?

SELECT * FROM Hiring WHERE MATCH(sample_content) AGAINST('군필남자' IN BOOLEAN MODE);

Search Result

sample_idsample_content
1주방보조 : 6개월 성실하고 오래 근무할 군필남자 직원 구합니다
3단기알바 : 쿠팡 물류센터 상하차 담당 군필남자 일일 알바 구합니다!(장기가능)

위 결과물 처럼 모든 row에 '군필남자'라는 단어가 들어가 있음에도 중간에 있는 2번째 row는 인식하지 못합니다.
이는 공백을 구분자로 단어를 구별하는 StopWord 검색의 특징 때문입니다.

Stopword검색엔진에서는 군필남자분 != 군필남자

Ngram Parser를 쓰기위해서는 WITH PARSER ngram이라는 명령어를 CREATE 혹은 ALTER TABLE을 사용할 때 명시해줘야합니다.

mysql> CREATE TABLE Hiring
(
        sample_id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
        sample_content TEXT,
        FULLTEXT INDEX ngram_idx(title) WITH PARSER ngram
) Engine=InnoDB CHARACTER SET utf8mb4;
 
mysql> ALTER TABLE Hiring ADD FULLTEXT INDEX ngram_idx(sample_cotent) WITH PARSER ngram;
mysql> CREATE FULLTEXT INDEX ngram_idx ON articles(sample_content) WITH PARSER ngram;

위 처럼 세 종류의 방식으로 Ngram parser 사용을 명시해 줄 수 있습니다.

SELECT * FROM Hiring WHERE MATCH(sample_content) AGAINST('군필남자' IN BOOLEAN MODE);

sample_idsample_content
1주방보조 : 6개월 성실하고 오래 근무할 군필남자 직원 구합니다
2테이블담당 : 업계 최고대우! 최소 1년이상 일하실 L호텔 테이블 담당 군필남자분 구합니다!
3단기알바 : 쿠팡 물류센터 상하차 담당 군필남자 일일 알바 구합니다!(장기가능)

이번에는 깔끔하게 원하는 결과가 나오게 됩니다.

주의점

Ngram parser를 이용한 FullText Search는 NATURAL LANGUAGE MODE를 사용하냐 BOOLEAN MODE를 사용하냐에 따라 결과값이 다르게 도출됩니다.

NATURAL LANGUAGE MODE같은 경우 Ngram Parser로 나눠진 토큰의 합집합으로 계산하기 때문에 의도치않은 결과값이 추가될 가능성이 존재합니다. 예를 들어 '군필남자'의 2gram Token은 다음과 같습니다.

군필 필남 남자

그러므로 '군필남자'말고 '군필여자' 혹은 '미필남자'도 검색값에 포함될 수 있습니다.

이와달리 BOOLEAN MODE의 경우에는 합집합 + Sequence를 고려합니다. 토큰의 합집합 중에서 순서대로 정확하게 끝까지 나열된 단어만 검색하므로 보다 정확한 값이 도출되게 됩니다.

참조 : https://mysqlserverteam.com/innodb-%EC%A0%84%EB%AC%B8-%EA%B2%80%EC%83%89-n-gram-parser/

profile
어려운 것은 없다, 다만 아직 익숙치않을뿐이다.

1개의 댓글

comment-user-thumbnail
2021년 6월 5일

큰 도움이 되었습니다. 감사합니다!

답글 달기