[MongoDB] $regex vs $text

저번달에 새로 추가한 기능이 커뮤니티와 유사한 것이었다. 워낙 촉박하게 만들어야해서 + 다큐먼트가 많이 생기지않을거라고 예상해서 $text 인덱스, 쿼리를 생각하지 않고 $regex만을 이용해서 만들어뒀다. 근데 새 기능을 오픈하자마자 예상보다 훨씬 빠르게 데이터가 쌓이고 있고, 인기가 많아지고 있다. 지금은 괜찮다만 한달만 있어도 $regex 쿼리가 느려질 것이 뻔히 보여 다른 방안이 필요하다는 생각이 들었다. 이 문제의 대응책으로 $text로 변경해야하나 고민하던 차에 둘의 차이를 정리해보려고 한다.



몽고DB에서 String의 부분을 가지고 검색할때 이용하는 두가지가 “$text”와 “$regex” 이다.

간단한 용례를 이렇다.

$regex 용례

// $regex

const searchResult = await Model.find({
    $text: { $search: "apple", $caseSensitive: false }
});

$text 용례

// $text

const searchResult = await Model.find({
    $regex: name, $options: "i"
})
// name은 정규표현식으로 검색할 필드, $options에 들어간 i는 caseSensitive: false를 의미.

딱봐도 어렵지않다. (몽고는 역시나 aggregate가 좀 헷갈리고 설계가 제일 힘들다.) 바로 둘의 차이로 넘어가자.


$regex 와 $text의 차이.

1. 초기 셋팅

용례에서 볼수 있듯이 $regex는 어떤 필드를 검색할지 쿼리문에 적은 후 검색한다. 그래서 딱히 셋팅해둘게 없다.

그런데 $text는 검색할 필드에 대해 미리 text 인덱스를 설정해놓아야만 한다. 이름과 이메일을 $text를 이용하여 검색하고 싶다면 아래처럼 인덱스 설정이 미리 되어있어야 한다.

UserSchema.index({email: text,name: text}, {weights: {email: 1,name: 2}})

검색하는 테이블에 text 인덱스가 하나도 설정이 되어있지 않다면 text index required for $text query 에러 메시지가 나타난다.


2. Full Text Search VS Partial Text Search

$regex는 partian text search가 된다.

$regex는 당연하게도 정규표현식 검색이다보니 부분적으로 단어가 맞아도 검색 결과에 포함된다. name: apple이 저장되어있을 때 app만 입력해도 apple을 찾는다. ppl을 입력해도, pp만 입력해도 말이다.

$text는 검색하는 단어가 100%(Full)로 맞는 것이 있어야 한다.

반면 $text는 Full text search이다보니 검색하는 값이 단어(스페이스) 기준으로 100% 같아야 한다. 예를 들면, content : "I have a apple" 이 데이터가 있을 때 { $text: { $search: "app" } }로 검색하면 결과값으로 아무것도 받을 수 없다. “I have a apple”를 스페이스 기준으로 분리된 체로 100% 매칭되는 것이 있어야만 찾을수 있다.

text index tokenizes and stems the terms in the indexed fields for the index entries. / mongodb docs

$text 쿼리로는 “apple”을 “app”입력만으로 찾을수 없다.

이렇게 따져보면 “$regex가 훨씬 디테일하게 값을 찾아서 좋은거 아냐?”라고 물을수 있다. 하지만 데이터가 많아진다면? Index를 이용해서 최대한 검색 범위를 줄이는 작업이 필요해진다. 근데 $regex는 index를 이용할 수 없다. 테이블내의 모든 데이터를 쿼리때마다 검색해야한다는 것이다. $regex는 데이터의 갯수가 어느정도 한계가 있을 때에만 이용할 수 있다. 혹은 다른 인덱스를 이용하여 값을 1차적으로 거른뒤에 걸러진 값내에서 regex를 이용하거나 말이다.

document가 많아지면 어쩔수 없이 $text 검색을 해야한다. $regex로는 너무 느려지니 아무것도 할수 없게 되버린다.


$regex는 너무 느리고 $text는 디테일이 떨어진다. 대응책은?

1. $text 검색부터하고 검색값이 없으면 $regex 검색을 해서 보완.

stacckoverflow 이 링크에 답변을 보면 $text와 $regex를 같이 쓰는 것도 꽤 괜찮다고들 한다. $text 검색이 $regex보다 훠~얼씬 빠르니 $text로 먼저 검색해보고 결과값이 시원치않으면 어쩔수 없이 $regex 검색을 하라는 것이다.

아래 코드는 ‘Recardo Canelas’이라는 개발자가 남긴 답변인데 여럿이 이 답글에 동의하고 있다.

import mongoose from 'mongoose'

const PostSchema = new mongoose.Schema({
    title: { type: String, default: '', trim: true },
    body: { type: String, default: '', trim: true },
});

PostSchema.index({ title: "text", body: "text",},
    { weights: { title: 5, body: 3, } })

PostSchema.statics = {
    searchPartial: function(q, callback) {
        return this.find({
            $or: [
                { "title": new RegExp(q, "gi") },
                { "body": new RegExp(q, "gi") },
            ]
        }, callback);
    },

    searchFull: function (q, callback) {
        return this.find({
            $text: { $search: q, $caseSensitive: false }
        }, callback)
    },

    search: function(q, callback) {
        this.searchFull(q, (err, data) => {
            if (err) return callback(err, data);
            if (!err && data.length) return callback(err, data);
            if (!err && data.length === 0) return this.searchPartial(q, callback);
        });
    },
}

export default mongoose.models.Post || mongoose.model('Post', PostSchema)


2. Elasticsearch를 이용하자….

Elasticsearch는 Full, Partial 모두 검색된다. ㅎㅎ




참고 자료