読者です 読者をやめる 読者になる 読者になる

Mongo2.4の全文検索

Qiitaにもポストしたんですがこちらにも記載。まだブログとQiitaとGistの使い分けが上手くできてない。みんなどうやって使い分けてるのだろうか?

何を検証する?

  • 全文検索の内容/設定 → 下記参照
  • 日本語の全文検索に対しては? → 未対応
  • 性能検証 → 未検証

機能特徴

  • 複数の文字列フィールドでテキストインデックス作成できる
  • 1つのコレクションに対して、1つの索引を作成できる
  • テキストインデックスは、アプリケーションがデータベースからドキュメントを
  • insert,update,deleteしたらリアルタイムで更新される。
    • テキストインデックスとクエリは言語固有のステミングとストップワードをサポートしている。例) the, an, and などをストップワードとしてdropする。(まだ単純な単語しかないみたい)
    • MongoDBは索引を挿入する間、複数言語をサポートした単純なステミングを使っている.MongoDBはクエリ実行前に自動的にテキストクエリを始める。
  • 対応言語は以下のとおり
    • danish
    • dutch
    • english
    • finnish
    • french
    • german
    • hungarian
    • italian
    • norwegian
    • portuguese
    • romanian
    • russian
    • spanish
    • swedish
    • turkish

デメリット

  • テキストインデックスは肥大化する。(各ドキュメントの各インデックス内に、ユニークな次のステムワードを持ってる)
  • インデックス作成する時間は通常のインデックスを作成するよりも時間がかかる。
  • データ挿入スループットは低下する。(MongoDBは各ドキュメントの各インデックス内に、ユニークな次のステムワードを追加しなければならないため)
  • テキスト検索はmongodの性能に影響があるだろう。特にNOT検索やフレーズマッチは効果的にインデックスを有効活用できない。

現時点の制約

  • テキストインデックスはドキュメント内の単語のち情報を格納してはならない。コレクション全体がRAMに収まると、効果的な検索ができる。
  • MongoDBは否定語やフレーズを軸にしない
  • インデックスは大文字/小文字を区別しない
  • コレクションは一度に一つのテキストインデックスしか持てない。
  • 結果はBSON Document Size(16MB)に収める必要がある。(後述するproject,filterなどを使用する)

コマンド

全文検索を使うには

db.adminCommand( { setParameter: 1, textSearchEnabled: true } )

もしくは、

mongod --setParameter textSearchEnabled=true

Index作成

  • content, subject インデックス対象
    • 別コレクションも対象にできる。
    • field名 : 1 もしくは-1と書くと昇順/降順の作成も付与できる。
  • name 索引名
  • weights 重み

また、ワイルドカード指定もできる。更に昇順/降順の索引も付与できる。

// ワイルドカード指定時
 db.contents.ensureIndex({ "$**":"text"}, {name:"ContentsIndex"});
db.contents.ensureIndex(
    {content: "text", subject: "text", <field>:1 or (-1)},
    {name:"ContentsIndex", weights:{content:10, subject:5}}
)
db.contents.getIndexes()
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "ns" : "blogs.contents",
        "name" : "_id_"
    },
    {
        "v" : 1,
        "key" : {
            "_fts" : "text",
            "_ftsx" : 1
        },
        "ns" : "blogs.contents",
        "name" : "ContentsIndex",
        "weights" : {
            "content" : 10,
            "subject" : 5
        },
        "default_language" : "english",
        "language_override" : "language",
        "textIndexVersion" : 1
    }
]

検索方法

db.<コレクション名>.runCommand("text", 
{ search: <string>,
filter: <document>,
project: <document>,
limit: <number>,
language: <string> } );
search

検索対象の文字列.半角スペースで区切るとorで検索を行う。Not検索はワードの前に"-"をつける。(例:A not B -> A -B)
また、A B Cという単語を検索する場合には "\"A B C \""とダブルクオーテーションを使用する。

filter

指定フィールドに指定文字列が含まれているデータを検索対象にする.
MongoDBで有効なドキュメントを使用できる。
filterの条件に一致するデータを出力する。
例えば fliter : { createAt: { $gt : new Date()}} という指定もできる。
先頭にasc or descとしてindexをつけた場合はfilter設定は必須かつ、equalの条件にする必要がある。
例えば、

db.contents.ensureIndex({ blogId:1, content: "text", subject: "text"})

というindexを作成した場合は、以下のようなコマンドしか受け付けない。

db.contents.runCommand("text" , { search: "indonesia", filter:{blogId : 100}});
db.contents.runCommand("text" , { search: "indonesia", filter:{blogId : {$gt:100}}});
{ "ok" : 0, "errmsg" : "BadValue need have an equality filter on: blogId" }
project

戻りフィールドを限定する.

limit

検索結果件数

language

トークンなどに関わってくる検索言語を指定.デフォルトは英語
今のところ日本語は対応していない。

出力内容

パラメータ名 内容
language 全文検索を行った言語
results 検索結果やスコアを格納した配列
results.score 検索結果のスコア
results.obj 検索結果のオブジェクト
stats ステータスを格納したオブジェクト
stats.nscanned 検索したindex数
stats.nscannedObjects 検索したドキュメント数(filterを設定するとカウントアップされる)
stats.n resultsの要素数. BSON Document Sizeを越えた場合、resultsの要素数より少なくなる場合がある
stats.nfound ドキュメントが一致している総数を返却する。BSON Document Sizeを越えた場合はstats.nより大きくなる場合がある
stats.timeMicro 検索にかかった時間(マイクロ秒)
ok 1 が返却される
db.contents.runCommand("text", { search: "america"});
{
	"queryDebugString" : "america||||||",
	"language" : "english",
	"results" : [
		{
			"score" : 7.5,
			"obj" : {
				"_id" : ObjectId("5151abf80e5ebfcf71b9f9f9"),
				"contentsId" : 10000,
				"blogId" : 100,
				"subject" : "アメリカ訪問",
				"content" : "Let's go America",
				"createAt" : 1364306936770
			}
		},
		{
			"score" : 7.5,
			"obj" : {
				"_id" : ObjectId("5151abf80e5ebfcf71b9f9fa"),
				"contentsId" : 11000,
				"blogId" : 100,
				"subject" : "アメリカ訪問",
				"content" : "遊びにいった i have been to Amarica",
				"createAt" : 1364306936775
			}
		},
		{
			"score" : 7.5,
			"obj" : {
				"_id" : ObjectId("5151abf80e5ebfcf71b9f9fb"),
				"contentsId" : 12000,
				"blogId" : 100,
				"subject" : "America訪問",
				"content" : "Amaricaへ遊びにいった i have been to Amarica",
				"createAt" : 1364306936779
			}
		}
	],
	"stats" : {
		"nscanned" : 3,
		"nscannedObjects" : 0,
		"n" : 3,
		"nfound" : 3,
		"timeMicros" : 96
	},
	"ok" : 1
}

contentsIdとcontentのみ出力を絞る

db.contents.runCommand("text", { search: "america", project: {contentsId:1, content:1}});
{
	"queryDebugString" : "america||||||",
	"language" : "english",
	"results" : [
		{
			"score" : 11.25,
			"obj" : {
				"_id" : ObjectId("5151abf80e5ebfcf71b9f9fc"),
				"contentsId" : 20000,
				"content" : "i have been to america. アメリカ言ってみた"
			}
		}
	],
	"stats" : {
		"nscanned" : 1,
		"nscannedObjects" : 0,
		"n" : 1,
		"nfound" : 1,
		"timeMicros" : 76
	},
	"ok" : 1
}