본문 바로가기

MongoDB

[MongoDB] 한글 전문 검색 수행하기

 

 

STEP#01. 한글과 전문 검색

 

전문 검색을 수행하기 위한 쿼리의 기능도 중요하지만, 전문 인덱스를 어떻게 구성하는지도 매우 중요하다.

 

도큐먼트가 저장될 때, 각 필드의 값을 분석해서 전문 인덱스를 구성하는 부분을 일반적으로 전문 파서( Parser )라고 한다.

 

전문 파서가 문장을 어떻게 검색 단위의 단어로 분리하고 가공하는지에 따라서 검색 결과라 천차 만별이 될 수도 있다.

 

MongoDB 서버의 전문 인덱스는 주요 언어에 대해서는 형태소 분석( Stemming ) 작업을 거쳐서 각 단어의 원형을 인덱스에 저장한다.

 

 

하지만 안타깝게도 한국어나 중국어 그리고 일본어 등에 대한 형태소 분석 기능은 제공하지 않는다.

 

MongoDB가 사용하는 형태소( Snowball, http://snowball.tartarus.org/ ) 자체가

 

영어를 포함한 주요 서구권 언어를 기준으로 개발되기도 했지만,

 

실제 한국어나 중국어 그리고 일본언에 대한 형태소 분석은 영어와 같이 단순한 규칙으로 형태소를 찾을 수가 없다.

 

그뿐만 아니라 한국어에 대한 형태소 분석기를 생성한다고 하더라도 배포된 용량과 시시각각 생성되고

 

변경되는 신조어를 따라가기는 매우 어려운 일이 될 것이다.

 

아마도 MongoDB 뿐만아니라 다른 오픈소스 데이터베이스에서도

 

한국어나 중국어 그리고 일본어 등에 대한 형태소 분석 기능을 지원하기는 상당히 어려울 것이다.

 

 

결국, MongoDB 서버에서 한글을 위한 전문 검색 기능은 n-Gram 형태의 전문 파서가 도입되지 않는 이상은 결국 어려울 것처럼 보일 수도 있다.

 

하지만 n-Gram 전문 파서는 언어의 특성과 전혀 관계없이 전문 검색을 가능하게 해주는 장점이 있지만,

 

지금 MongoDB 서버가 제공하고 있는 구분자 기준의 전문 파서보다 훨씬 많은 인덱스 키를 만들어내기 때문에 성능적인 이슈가 문제 될 수도 있다.

 

그뿐만 아니라 n-Gram 파서를 언제 MongoDB 서버가 지원하게 될지도 모르는 일이다.

 

 

그렇다면 MongoDB 서버에서 전문 검색을 포기해야 하는 것일까?

 

그마나 한글을 위한 조그마한 돌파구는 있는 듯하다.

 

다음 간단한 예제를 살펴보자.

 

mongo> db.stores.createIndex( { name : "text", description : "text" } );

mongo> db.stores.insertOne(
  {
      name : "알레그리아"
    , description : "원두 로스팅을 주로 하다가, 지금은 직영 커피숍깢까지 운영한다."
  }
);

mongo> db.stores.find( { $text : { $search : "로스팅" } } );
==> Not Found

mongo> db.stores.find( { $text : { $search : "로스팅을" } } );
{
    "name" : "알레그리아"
  , "description" : "원두 로스팅을 주로 하하다가, 지금은 직영 커피숍까지 운영한다."
}

 

MongoDB 서버는 한글 데이터를 저장하면 내장된 형태소 분석기를 거친다 하더라도

 

실제 한글의 형태소 분석 기능을 가지고 있지 않기 때문에 형태소 분석 기능을 가지고 있지 않기 때문에

 

결국 입력된 문자열을 공백 문자를 기준으로 잘라서 각 단어를 인덱스에 저장하는 역할만 수행한다.

 

그래서 결국 위의 예제 데이터는 다음과 같은 단어를 인덱스에 저장하는 수준의 처리만 한다.

 

알레리아, 원두, 로스팅을, 주로, 하다가, 지금은, 직직영, 커피숍까지, 운영하한다.

 

그런데, 이 예제에서 확인할 수 있듯이 MongoDB의 전문 검색은 완전히 일치하는 인덱스 엔트리만 찾아서 결과를 반환한다.

 

그래서 "로스팅"이란 단어로 전문 검색을 실행했을 떄 아무런 결과가 반환되지 못한 것이다.

 

하지만 한글의 조사인 "을"까지 포함해서 "로스팅을"로 검색하면 완전히 일치하는 경우이므로 원하는 결과를 가져올 수 있다.

 

지금까지 결과만 놓고 보면 MongoDB 서버 자체적인 기능으로는 한글을 위한 전문 검색을 사용할 수가 없다.

 

하지만 실제 MongoDB의 전문 검색이 검색어를 기준으로 범위 검색을 수행하게 된다면

 

한글을 위한 MongoDB의 전문 검색 기능의 사용성은 많이 높아진다.

 

  • 기존 MongoDB의 검색 일치 기준 : 검색어와 일치( index-entry == "로스팅" )
  • 개선된 MongoDB의 검색 일치 기준 : 범위 일치 ( "로스팅" <= index-entry < "로스팅" )

 

MongoDB가 이렇게 검색어에 대해서 일정 범위 검색을 수행할 수 있다면

 

한글에서 사용되는 조사가 어떤 것이라 하더라도 일치된 결과를 가져올 수도 있다.

 

그리고 운 좋게도 주로 단어의 후미에 조사만 사용하는 한글에서는

 

이 정도만 지원되더라도 사실 대부분의 경우 별다른 어려움 없이 사용할 수 있을 것이다.

 

하지만 이 정도의 오차는 기존 형태소 분석을 수행하는 경우라 하더라도 충분히 나타날 수 있는 수준의 오차다.

 

만약 MongDB에 한글을 위한 전문 검색 기능이 추가되기 전에 한글 전문 검색 기능이 필요하다면

 

다음 예제와 같이 전문 검색 필드의 값을 배열 타입의 값으로 변환해서 정규 표현식 검색을 적용하는 것도 방법일 수 있다.

 

mongo> db.articles.insertOne(
  {
      article_id : 1
    , title : "MongoDB는 괜찮은 데이터베이스 서버입니다."
    , body : "..."
    , keywords : [ "MongoDB는", "괜찮은", "데이터베이스", "서버입니다." ]
  }
);

mongo> db.articles.createIndex( { keywords : 1 } );
mongo> db.articles.find( { keywords :/^MongoDB/ } ).pretty();

 

위 예제는 title 필드의 내용에 대해서 전문 검색을 실행하고자 하는 경우인데,

 

만약 title 필드에 대해서 이렇게 정규 표현식조건을 사용하면 인덱스를 사용하지 못하거나

 

일치하는 도큐먼트를 조회할 수 없게 된다.

 

이런 경우 title 필드의 문자열을 공백이나 다른 문장 기호 단위로 잘라서 문자열 배열로 keyworkds라는 필드에 별도로 저장하고,

 

keywords 필드에는 멀티 키 인덱스를 생성한 다음 검색은 정규 표현식 조건을 이용해서 검색하면 된다.

 

이때 정규 표현식은 반드시 프리픽스 일치( PrefixMatch )형태를 사용해야 인덱스를 정상적으로 사용할 수 있다.

 

그래서 정규 표현식 조건은 "^"로 시작하고 검색어 다음에는 아무런 정규 표현식이 없어야 한다.

 

 


 

STEP#02. 한글과 n-Gram 인덱스

 

지금까지 살펴본 이유로 인해서 MongoDB 서버가 가진 형태소 분석 기반의 전문 검색 기능은

 

한글을 포함한 중국어나 일본어에는 적합하지 않다.

 

물론 우회할 방법은 있지만, 완전한 해결책은 아닐 것으로 보인다.

 

그래서 여기에서는 MongoDB 서버의 코드 한글을 위한 n-Gram 전문 인덱스를 위한 기능을

 

직접 추가해서 사용하는 방법을 간단히 살펴보겠다.

 

 

우선 전문 검색 인덱스를 관리하기 위해서는 구분자와 토크나이저가 적절히 엽력된 문자열에서

 

단위 문자열( Term )을 분리하는 과정을 거친다.

 

이렇게 분리된 단위 문자열( Term )을 전문 인덱스에 저장하고, 사용자가 검색어를 입력하면

 

그 검색어 또한 같은 과정을 거쳐서 단위 문자열로 분리되고, 검색을 수행하게 된다.

 

간단히 "This is MongoDB books"라는 문장이 구분자( Delimiter ) 기반으로

 

토크나이징 되는 경우( 기존MongoDB의 인덱싱 방식 )와

 

bi-Gram( 2글자씩 토크나이징 하는 n-Gram 방식 )으로 토크나이징 되는 경우의 차이를 그림으로 살펴보자.

 

 

 

 

위 그림은 기존 MongoDB 서버에서 사용하고 있는 방식으로,

 

지정된 구분 문자( Delimiter ) 단위로 단어를 잘라서 불용어( Stopword )를 버리고 형태소 분석 과정을 거쳐서

 

최종 인덱싱될 단위 문자열( Term )을 만드는 과정을 보여주고 있다.

 

여기에서 정규화( Normalize ) 과정은 형태소 분석뿐만 아니라 대소문자나 액센트 문자 등에 대한 정규화 과정까지 포함한다.

 

 

 

위 그림은 2글자씩 잘라서 n-Gram인덱스를 생성하는 bi-Gram방식을 보여주고 있는데,

 

n-Gram 방식의 토크나이징과 인덱싱 방식은 기존 MongoDB 서버에서 처리하는

 

구분자 기반의 토크나이징과 불용어 필터링 과정을 모두 거친다.

 

이렇게 필터링해서 남은 문자열( "MongoDB"와 "books" )에 대해서 bi-Gram 토크나이징 처리가 실행되며,

 

최종적으로 생성된 2글자 단위 문자열( Term )을 전문 인덱스에 추가하게 된다.

 

다시위 그림을 보면 이상적인 형태의 n-Gram인덱싱 과정을 보여주고 있다.

 

 

다음 아래 그림은 n-Gram인덱스가 실제 작동하는 방식은 불용어 처리과정이 포함돼 있지 않다.

 

그래서 이상적인 방식의 토크나이징보다는 최종 단위 문자열 개수가 많으며,

 

그만큼 인덱스 엔트리의 개수가 많아질 수 있다. 하지만 이 차이가 사용성을 저해할 만큼의 차이가 있는 것은 아니다.

 

이제 시작할 예제부터는 더 최적화된 n-Gram 전문 인덱스를 구현하고자 할 때 추가로 고려해야 할 사항으로 생각하자.

 

 

위 그림 에서 볼 수 있듯이 n-Gram 토크나이징의 결과에서 중복된 단위 문자열( Term )은

 

중복이 제거되고, 하나의 유니크한 단위 문자열만 전문 인덱스에 추가된다는 것도 기억하자.

 

 

최종적으로 위 그림과 같은 방식의 n-Gram 전문 인덱스를 구현하는 소스 코드는 아래의 깃헙을 참조하자.

 

n-Gram인덱스 기능을 가진 MongoDB 서버를 빌드하는 방법은 기존 MongoDB 서버의 빌드 과정과 동일하며,

 

별도로 공유 라이브러리를 활성화하거나 플러그인을 등록하는 과정이 필요하지 않다.

 

아래의 깃헙 코드를 포함하는 MongoDB를 빌드하면 이미 빌드된 MongoDB 서버에 n-Gram 전문 인덱스 기능이 내장돼 있기 때문이다.

 

https://github.com/SunguckLee/Real-MongoDB/commit/bd994fcb2e8b7846ea16389dab3593aebe3c50ac

 

이제 빌빌드된 MongoDB 서버에서 n-Gram 인덱스를 생성하고 활용하는 과정을 살펴보자.

 

우선 n-Gram 인덱스를 사용하려면 다음 예제처럼 전문 인덱스를 생성할 때 "default_language" 옵션을  "ngram"으로 명시해야 한다. 

 

"default_language"옵션을 별도로 설정하지 않으면 "english"가 기본값으로 설정되는데,

 

이 경웅에는 영어를 위한 토크나이징과 형태소 분석 과정을 거쳐서 전문 인덱스 처리된다.

 

// n-Gram 전문 인덱스
// 인덱스는 반드시 "default_language : 'ngram' " 옵션을 명시해야 한다.
mongo> db.ngram.createIndex( { content : "text" }, { default_language : "ngram" );

// 테스트 데이터 저장
mongo> db.ngram.insertOne( { "content" : "MongoDB는 좋은 비관계형 데이터베이스 서버입니다." } );
mongo> db.ngram.insertOne( { "content" : "MySQL은 좋은 관계형 데이터베이스 서버입니다." } );

 

getIndxes( ) 명령으로 컬렉션의 인덱스 목록을 확인해보면 전문 검색 인덱스의 "default_language"  옵션이 "ngram"으로 설정된 것을 확인할 수 있다.

mongo> db.ngram.getIndexes();

 

 

이제 n-Gram전문 인덱스를 가진 ngram 컬렉션에 예제 데이터를 저장한다고 간단한 검색을 테스트해보자.

 

// 2개의 도큐먼트가 모두 일치하는 검색
mongo> db.ngram.find( { $text : { $search : "관계" } } );
{ "content" : "MongoDB는 좋은 비관계형 데이터베이스 서버입니다." }
{ "content" : "MySQL은 좋은 관계형 데이터베이스 서버입니다." }

// 1개의 도큐먼트만 일치하는 검색
mongo> db.ngram.find( $text : { $search "비관계" } } );
{ "content" : "MongoDB는 좋은 비관계형 데이터베이스 서버입니다." }

// 문자열열의 일부( Suffix )만 일치하는 검색
mongo> db.ngram.find( $text : { $search "베이스" } } );
{ "content" : "MongoDB는 좋은 비관계형 데이터베이스 서버입니다." }
{ "content" : "MySQL은 좋은 관계형 데이터베이스 서버입니다." }

 

물론 n-Gram 인덱스를 사용하는 경우에도 불리언 검색을 수행할 수 있다.

 

// 불리언 연산자로 1건의 도큐먼트만 일치하는 검색
mongo> db.ngram.find( $text : { $search : "베이스 -비관계" } } );
{ "content" : "MySQL은 좋은 관계형 데이터베이스 서버입니다." }

mongo> db.ngram.find( $text : { $search : "베이스 -관계" } } );
==> Not Found

 

이미 설명했듯이 여기에서 소개하는 n-Gram 인덱스는 2글자씩 자랄서 인덱싱하는 bi-Gram 토크나이저를 사용한다.

 

그래서 한 글자로 구성된 검색어는 어떤 경우에도 일치된 결과를 가져오지 못한다.

 

n-Gram 인덱스의 특성상 토크나이저가 사용하는 문자열의 길이보다 짧은 길이의 문자열은 도큐먼트를 찾을 수 없지만,

 

2글자 이상의 검색어에 대해서는 일치하는 도큐먼트를 찾을 수 있는 것이다.

 

// 단일 문자 키워드 검색
mongo> db.ngram.find( $text : { $search : "관" } } );
==> Not Found

 

만약 1글자 키워드로 검색할 수 있게 하고자 한다면 NGRAM_TOKEN_SIZE를 1로 변경해서 다시 컴파일 하면 된다.

 

물론 n-Gram 토크나이저가 사용하는 토큰의 길이를 인덱스별로 다르게 설정하도록 소스 코드를 변경해서 사용하는 것도 좋은 방법이다.

 

하지만 토큰 사이즈가 줄어들수록 인덱스의 크기는 조금 더 작아질 수 있지만,

 

검색어에 일치하는 결과가 많아져서 MongoDB 서버의 내부적인 필터링 시간이 오래 걸리게 된다.

 

반대로 토큰 사이즈가 클수록 인덱스의 크기는 조금 더 커지겠지만, 좀 더 정확한 결과를 찾아서

 

MongoDB 서버의 내부적인 필터링 시간을 급격하게 줄여줄 수 있다.

 

그래서 서비스의 요건이 3글자 이상의 키워드만 검색한다면

 

n-Gram 토큰의 크기를 3으로 변경해서 인덱스를 사용하는 것이 훨씬 효율적이고, 빠르게 검색될 것이다.