본문 바로가기

MongoDB

[MongoDB] 전문검색( Fulltext Search )

STEP#01. 전문 검색

 

전문검색이란?

• 여러 문서에서 특정의 문자열을 검색하는 것
• 여러 문서에 걸쳐 문서에 포함되는 전문을 대상으로한 검색

 

 

RDBMS 처럼 MongoDB 서버도 전문 검색을 위해서 전문 검색 인덱스와

 

전문 검색을 위한 쿼리 문법을 제공하고있다.

 

우선 MongoDB의 전문 검색을 사용하는 방법을 간단히 살펴보자.

 

// 전문 검색 예제 데이터 저장
mongo> db.stores.insertMany(
  [
      { _id : 1, name : "Java Hut", description : "Coffee and cakes" }
    , { _id : 2, name : "Burger Buns", description : "Gourmet hamburgers" }
    , { _id : 3, name : "Coffee Shop", description : "Just coffee" }
  ]
);

// 전문 검색 예제 인덱스 생성
mongo> db.stores.createIndex( { name : "text" } );

// 전문 검색 쿼리 실행
mongo> db.stores.find( { $text : { $search : "java coffee" } } ).sort( { "_id": 1 } );
{ "_id" : 1, "name" : "Java Hut", "description" : "Coffee and cakes" }
{ "_id" : 3, "name" : "Coffee Shop", "description" : "Just coffee" }

 

위 예제에서 stores 컬렉션에 전문 검색을 위한 데이터를 저장하고, 컬렉션의 name 필드에전문 검색을 위한 인덱스를 추가했다.

 

전문 검색 인덱스를 생성할 때는 인덱스를 생성할 필드의 이름 뒤에 전문 검색 인덱스를 의미하는 키워드인 "text"를 입력하면된다.

 

그리고 마지막으로 전문 검색 쿼리를 작성할 때에는 "$text” 연산자를 이용해서 검색어를 입력하면

 

"java"와 "coffee"를 포함하는 조건의 OR 연산이 실행된다. 

 

사실 전문 검색 인덱스의 생성과 쿼리 사용자체는 매우 단순하다.

 

 

MongoDB의 전문 검색 인덱스는 컬렉션당 하나만 생성할 수 있다.

 

만약 2개 이상의 필드에 대해서 검색할 수 있는 전문 검색 인덱스를 생성하고자 한다면

 

다음과 같이 인덱스를 생성할 때 필요한 필드들을 모두 나열하면 된다.

 

다음 예제는 name 필드의 값과 description 필드 값에 대해서 전문 검색이 가능한 인덱스를 생성한 것이다.

 

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

 

만약 컬렉션의 모든 필드에 대해서 전문 검색이 가능한 인덱스를 생성하고자 한다면

 

다음과 같이 필드명 대신 "$**"를 명시하면된다.

mongo> db.stores.createIndex( { "$**" : "text" } );

 


 

STEP#02. 불리언 검색

 

일반적으로 전문 검색 기능에서 많이 사용되는 기능은 검색어에 대한 불리언 연산과 필드별 중요도 설정일 것이다.

 

MongoDB의 전문 검색 기능에서도 이러한 기능은 동일하게 상요할 수 있다.

 

우선 MongoDB의 전문 검색 엔진에서 불리언 연산은 다음과 같이 "-" 부호를 이용해서 검색 대상에서 제외할 수 있다.

 

mongo> db.stores.find( { $text : { $search : "java coffee shop" } } ).sort( { "_id": 1 } );
{ "_id" : 1, "name" : "Java Hut", "description" : "Coffee and cakes" }
{ "_id" : 3, "name" : "Coffee Shop", "description" : "Just coffee" }

mongo> db.stores.find( { $text : { $search : "java coffee -shop" } } ).sort( { "_id": 1 } );
{ "_id" : 1, "name" : "Java Hut", "description" : "Coffee and cakes" }

 

예제의 첫 번째 쿼리에서는 "java"나 "coffie"그리고 "shop" 단어를 포함하는 모든 결과를 반환할 것이다.

 

이때 특별히 불리언 연산자 없이 검색을 실행하면 나열된 모든 단어의 OR 연산을 수행한다.

 

그런데 두 번째 쿼리처럼 특정 단어 앞에 "-" 부호를 넣어서 검색하면 다른 단어는 포함하지만

 

해당 단어( "-" 부호를 가진 단어 )는 포함하지 않는 결과를 검색할 수 있다.

 

이때 부정 검색을 위해서는 "-" 부호 앞에 반드시 공백을 추가해야 한다.

 

예를 들어. "coffee-shop"이란 검색어로 검색하면 부정 검색이 아니라 "coffee-shop"이란 단어가 포함된 도큐먼트를 검색한다.

 

그리고 OR 연산이 아니라 AND 연산을 수행하고자 한다면 다음과 같이 문장 검색( phrase search )을 실행한다.

 

MongoDB에서 문장 검색을 위해서는 이중 따옴표( " )로 검색어를 감싸주면 된다.

 

그런데 이미 따옴표가 사용되었으므로 이를 회피( Escape character )하기 위해서 \" 를 사용한다.

 

mongo> db.stores.find( { $text : { $search : "\"coffee\" \"shop\"" } } ).sort( { "_id": 1 } );
{ "_id" : 3, "name" : "Coffee Shop", "description" : "Just coffee" }

 

그런데 문장 검색과 단어 검색을 조합하면 일반적으로 생각할 수 있는 형태가 아닌 조금 복잡한 형태로 검색이 이뤄진다.

 

예를 들어, 다음과 같이 "coffee shop"과 alegria 그리고 starbucks라는 단어를 검색하면

 

"coffee shop" AND ( "alegria" OR "starbucks" OR "coffee" OR "shop" )의 조합으로 검색이 수행된다.

 

그래서 아래 쿼리의 결과는 "coffee shop" 이란 구절은 무조건 포함하고,

 

거기에 추가로 starbucks나 alegria란 단어를 포함한 결과만 가져온다.

 

mongo> db.stores.find( { $text : { $search : { "\"coffee shop\" alegria starbucks" } } } );
{ 출력결과 없음 }

 


 

STEP#03. 중요도( Weight ) 설정

 

MongoDB에서 전문 검색 쿼리를 수행할 때,

 

일치하는 검색어가 어떤 필드에 저장된 값인지에 따라서 중요도( Weight )를 설정할 수 있다.

 

name 필드는 중요도가 2이며, description 필드는 중요도를 1로 설정했다.

 

이렇게 하면 검색어가 name 필드에서 일치하는 경우 description 필드보다 2배의 영향도를 미치게 된다.

 

즉 중요도가 높을수록 검색 일치 영향도가 높다.

 

필드 별로 설정된 중요도는 db.stores.getIndexes( ) 명령으로도 확인해 볼 수 있다.

 

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


mongo> db.stores.getIndexes();
[
  {
      "v" : 2
    , "key" : {
      "_id" : 1
    }
    , "name" : "_id_"
    , "ns" : "testDB.stores"
  }
  , {
      "v" : 2
    , "key" : {
      "_fts" : "text",
      "_ftsx" : 1
    }
    , "name" : "name_text"
    , "ns" : "testDB.stores"
    , "weights" : {
      "name" : 1
    }
    , "default_language" : "english"
    , "language_override" : "language"
    , "textIndexVersion" : 3
  }
]

 

이제 중요도가 서로 다르게 설정된 전문 인덱스를 이용해서 검색할 때, 검색 스코어( Score )를 확인하는 방법을 살펴보자.

 

mongo> db.stores.find(
    { $text : { $search : "coffee" } }
  , { score : { $meta : "textScore" } }
).sort( { score : { $meta : "textScore" } } );

 { "_id" : 3, "name" : "Coffee Shop", "description" : "Just coffee", "score" : 2.25 }
 { "_id" : 1, "name" : "Java Hut", "description" : "Coffee and cakes", "score" : 0.75 }

 

MongoDB에서 전문 검색의일치 스코어는 "{ $meta : "textScore" }"로 확인할 수 있는데,

 

위의 예제에서는 일치 스코어 값을 "score"라는 필드로 결과에 포함시켰다.

 

그리고 일치 스코어 값으로 정렬하기 위해서 "{ score : { $meta : "textScore" } }"로 정렬해서 결과를 가져오게 했다.

 

쿼리의 결과 프라이머리 키가 3인 도큐먼트는 일치 스코어가 2.25라는 값이 나왔다.

 

이는 name 필드에서 "coffee"단어가 일치됐기 때문에 name필드의 중요도( weight ) 2와

 

description 필드의 중요도 1이 MongoDB 내부적인 스코어링 알고리즘을 거쳐서 일치 스코어 2.245가 된 것이다.

 


 

STEP#04. 전문 인덱스 성능

 

전문 인덱스는 입력된 도큐먼트에서 검색 대상이 되는 필드의 값들을 파싱하고

 

형태소 분석을 거친다음 유니크한 키워드( 단어 )만 모아서 인덱스 엔트리로 저장한다.

 

물론 하나의 도큐먼트에서 반복된 단어가 많이 사용되면 그만큼 인덱스 크기를 줄일 수 있다.

 

하지만 전문 인덱스는 주로 크기가 큰 도큐먼트를 파싱하여 인덱싱하므로

 

인덱스가 매우 커지고 처리시간이 많이 소요될 수도 있다.

 

예를 들어, 인터넷 웹 페이지를 저장하는 컬렉션에 대해서 전문 인덱스를 생성한다면

 

이 컬렉션의 도큐먼트 하나하나는 상당히 큰 값을 가질 것이다.

 

대략 하나의 도큐먼트가 1,000개 정도의 단어를 가지고 있다고 가정하면

 

이 컬렉션의 도큐먼트가 100만건이면 전문 인덱스는 전체 10억개의 인덱스를 엔트리를 관리해야 한다.

 

그뿐만 아니라 도큐먼트 하나가 저장되고 삭제될 때마다 전문 인덱스에서 인덱스 키를 1,000개씩 저장하거나 삭제해야 한다.

 

이는 일반 컬렉션에 도큐먼트 1,000건을 저장하고 삭제하는 것과 거의 동일한 부하를 만들게 된다.

 

 

MongoDB 서버를 일반적인 서비스용 데이터베이스로 사용하면서 보조적인 기능으로

 

전문 검색 기능을 사용하는 경우라면 이런 대용량 전문 검색 기능은 피하는 것이 좋다.

 

즉 전용의 전문 검색용 데이터베이스를 별도로 구성하는 것이 좋을 수도 있다.

( 물론 전문 검색을 위한 별도의 솔루션을 사용하는 것도 방버일 수도 있다. )

 

더구나 MongoDB 서버는 전문 인덱스를 샤드 키로 선택할 수 없기 때문에, 

 

전문 인덱스 기능만을 분산 처리하기는 쉽지 않을 수 있다.

 

이런 요건은 MongoDB뿐만 아니라 MySQL과 Oracl과 같은 온라인 트랜잭션을 주목적으로 하는

 

데이터베이스에 모두 해당하는 이야기일 것이다.

 

MongoDB의 전문 검색은 크지 않은 도큐먼트에 대해서 전문 검색을 사용해야 하는데,

 

별도의 전문 검색 솔루션이나 장비를 투입하는 것이 부담스러운 경우 보조적인 용도로 고려하는 것이 좋다.

 

이는 MongoDB 서버가 전문 검색 처리 전용으로 개발된 데이터베이스가 아닐뿐만 아니라,

 

온라인 트랜잭션을 처리하는 데이터베이스는 주로 고성능의 장비를 사용하므로

 

서버 자원( 주로 높은 사양의 디스크 )의 낭비가 심해질 수도 있다.

 

 

MongoDB 쿼리에서 "$text" 명령을 사용하면 전문 검색이 사용되므로 별도로 인덱스 사용에 대한 힌트를 사용할 필요가 없다.

 

즉 쿼리 문장의 문법을 기준으로 전문 검색 인덱스가 사용될 것인지 아닌지가 판단되는 것이다.

 

그리고 전문 검색 인덱스를 검색한 결과는 어떤 형태로든지 정렬이 보장되지 않는다.

 

그래서 다음 쿼리처럼 전문 검색을 수행하면 정렬이 필요한 경우,

 

MongoDB는 별도의 정렬 처리를 수행한 후에야 결과를 반환할 수 있다.

 

mongo> db.stores.find( { $text : { $search : "로스팅" } } ).sort( { name : 1 } );

 

 

MongoDB의 전문 인덱스는 다른 일반 필드와 함께 복합 필드 인덱스를 생성할 수도 있다.

 

예를 들어, 주제별 게시판을 서비스한다고 가정해보자. 이때 전문 검색을 주제별로만 수행한다면

 

다음과 같이 게시판의 주제와 게시물의 내용을 같이 묶어서 전문 인덱스를 생성할 수도 있다.

 

mongo> db.articles.createIndex( { category : 1, title : "text" } );

mongo> db.articles.insertOne( { category : "MongoDB", title : "Query tuning" } )
mongo> db.articles.insertOne( { category : "MySQL", title : "Query tuning" } );

 

이렇게 일반 B-Tree 인덱스( Ascending Descending Index ) 필드인 주제( category ) 필드와 묶어서

 

전문 인덱스를 생성하면 주제 단위로 검색하는 쿼리는 다른 주제에 속한 게시물과는 완전히 구분된 인덱스 범위를 검색하면 된다.

 

즉 실제 물리적으로는 하나의 인덱스이지만 논리적으로는 주제별로 서로 다른 인덱스가 관리되는 것처럼 작동하는 것이다.

 

다음과 같이 category가 "MongoDB"인 경우를 검색하면 전문 인덱스의 전체 크기에 상관없이

 

MongoDB의 주제의 게시물들과 연관된 인덱스에서만 "Query"라는 단어를 검색하면 되므로 검색 성능을 획기적으로 향상시킬 수 있다.

 

mongo> db.articles.find( { category : "MongoDB", $text : { $search : "Query" } } );
{ "category" : "MongoDB", "title" : "Query tuning" }

mongo> db.articles.find( { category : "MySQL", $text : { $search : "Query" } } );
{ "category" : "MySQL", "title" : "Query tuning" }

 

하지만 이렇게 B-Tree 인덱스 필드를 전문 인덱스보다 앞쪽에 위치시켜서 인덱스를 생성하면

 

전문 검색 쿼리에서 반드시 선행 필드의 조건을 포함해야만 전문 인덱스를 이용할 수 있게 된다.

 

즉 category 필드를 선행 필드로 포함시켜서 전문 인덱스를 생성한 경우에 다음과 같은 쿼리는 전문 인덱스를 사용할 수 없다.

 

이 경우 쿼리의 성능이 떨어지는 것이 아니라.

 

다음과 같이 쿼리 자체가 실패하게 된다는 것에 주의하자.

 

mongo> db.articles.find( { $text : $search : "Query" } } );
Error: error : {
    "ok" : 0
  , "errmsg" : "srror processing query: ns=test.articlesTree: TEXT : query=Query, language=english, caseSensitive=0, diacriticSensitive=0, tag=NULL\nSort: {}\nProj: {}\n plannerreturned error: failed to use text index to satisfy $text query (if text index is compound, areequality predicates given for all prefix fields?)"
  , "code" : 2
  , "codeName" : "BadValue"
}