MongoDBノート - $regex を使ったクエリでのインデックスの使われ方

昨日の記事で

Mongoidノート - validates_uniqueness_of で :case_sensitive => false を指定すると…… - Alone Like a Rhinoceros Horn


simple prefix queries (also called rooted regexps) like /^prefix/ のような場合を除きインデックスは使われない(使えない)ので、:case_sensitive => false を指定した場合、


validates_uniqueness_of は validation に際して、フィールドの値がユニークであることを保証するために、$regex を用いたクエリでコレクションの全ドキュメントをスキャンすることになる。

と書いた。

公式のドキュメント

によれば、simple prefix queries (also called rooted regexps) like /^prefix/ な場合はインデックスを使うと記載があるものの、そうでない場合については、クエリを最適化するともしないとも書かれていないので、実際に全ドキュメントをスキャンするのかどうか検証してみることにした。

もしかしたら、/^pattern$/i に対し、スキャンの範囲を [p, q) と [P, Q) に絞る、みたいな最適化をやっているかも知れない。

ということで、1002件のデータが登録された usersコレクションで実験してみる。( n はユーザー名)

MongoDB のバージョンは

% mongod --version                                                                                 
db version v2.0.0, pdfile version 4.5
Thu Dec 15 20:18:28 git version: 695c67dff0ffc361b8568a13366f027caa406222


まずは普通にインデックスを効かせたクエリ

> db.users.find( { n: 'mills_hector' } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 0,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"mills_hector",
				"mills_hector"
			]
		]
	}
}

インデックスがちゃんと使われていることが確認できる。

/^prefix/

次に、インデックスを使うとされている simple prefix queries (also called rooted regexps)

> db.users.find( { n: { $regex: /^mills_hector/ } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 0,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"mills_hector",
				"mills_hectos"
			]
		]
	}
}

ちゃんとインデックスが効いている。indexBounds にも注目。

/^pattern$/i

今回検証したかった、/^pattern$/i

Mongoid で validates_uniqueness_of にて :case_sensitive => false を指定した場合、validation時にこれと同様のクエリが発行される。

> db.users.find( { n: { $regex: /^mills_hector$/i } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1002,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 5,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"",
				{
					
				}
			]
		]
	}
}

nscannedObjects は 1 だが、nscanned が 1002

Explain - MongoDB


nscanned

Number of items (documents or index entries) examined. Items might be objects or index keys. If a "covered index" is involved, nscanned may be higher than nscannedObjects.


nscannedObjects

Number of documents scanned.

とあることからもわかる通り、これはインデックスエントリの全件がスキャンされたことを示している。
ドキュメント全件分のスキャンが必要になるものの、対象フィールドに対してインデックスがはってあるため、スキャンはインデックス上のエントリに対して行われる。(なので、実際にスキャンされたドキュメントの数を示す nscannedObjects は 1 になっている)

インデックスを削除すると……
> db.users.dropIndex({n:1});
{ "nIndexesWas" : 2, "ok" : 1 }
> db.users.find( { n: { $regex: /^mills_hector$/i } } ).explain();
{
	"cursor" : "BasicCursor",
	"nscanned" : 1002,
	"nscannedObjects" : 1002,
	"n" : 1,
	"millis" : 3,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		
	}
}

cursor が BasicCursor となり、nscannedObjects も 1002 になった。これは全ドキュメントを実際にスキャンしたことを示している。

i を付けない場合は?

(インデックスを再度設定して続行)

> db.users.find( { n: { $regex: /^mills_hector$/ } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 0,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"mills_hector",
				"mills_hectos"
			]
		]
	}
}

先頭にマッチする ^ があるのでこれは /^prefix/ と同じ。indexBounds に注目。

^ をなくすと?
> db.users.find( { n: { $regex: /mills_hector$/ } } ).explain();
{
	"cursor" : "BtreeCursor n_1",
	"nscanned" : 1002,
	"nscannedObjects" : 1,
	"n" : 1,
	"millis" : 5,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"isMultiKey" : false,
	"indexOnly" : false,
	"indexBounds" : {
		"n" : [
			[
				"",
				{
					
				}
			]
		]
	}
}

nscanned が 1002
^ がなければ indexBoudns によるスキャン範囲の絞り込みができなくなるため、インデックスエントリの全件をスキャンしなければならなくなった。

まとめ
  • $regex を使ったクエリでは、/^prefix/ な場合を除き全件スキャン
    • インデックスあり → コレクションの全インデックスエントリをスキャン
    • インデックスなし → コレクションの全ドキュメントをスキャン
  • explain() 便利!

Mongoidノート - validates_uniqueness_of で :case_sensitive => false を指定すると……

Mongoid の validates_uniqueness_of で :case_sensitive => false を指定するとインデックスが使われないような予感がしたので、確認のためソースを追ってみた。

# File: mongoid-2.3.4/lib/mongoid/validations.rb

      def validates_uniqueness_of(*args)
        validates_with(UniquenessValidator, _merge_attributes(args))
      end

UniquenessValidator の方を見てみる。

# File: mongoid-2.3.4/lib/mongoid/validations/uniqueness.rb

      def validate_each(document, attribute, value)
        if document.embedded?
          # snip	
        else
          criteria = klass.where(criterion(document, attribute, value))
          criteria = scope(criteria, document, attribute)
          document.errors.add(attribute, :taken) if criteria.exists?
        end
      end

if criteria.exists? ならエラーと。

# File: mongoid-2.3.4/lib/mongoid/validations/uniqueness.rb

      def criterion(document, attribute, value)
        { attribute => filter(value) }.tap do |selector|
          if document.persisted? ||
            (document.embedded? && (document.primary_key != Array.wrap(attribute)))
            selector.merge!(:_id => { "$ne" => document.id })
          end
        end
      end

criterion は where に渡すハッシュを返すメソッドで

{ :name => "David_Thomas" } 

みたいなのを返す。

で、:case_sensitive => false ならどうなるんだ?

# File: mongoid-2.3.4/lib/mongoid/validations/uniqueness.rb

      def filter(value)
        !case_sensitive? && value ? /^#{Regexp.escape(value.to_s)}$/i : value
      end

わわわ。正規表現になるのかよ。これだとインデックスが使えないはず。

Advanced Queries - MongoDB


For simple prefix queries (also called rooted regexps) like /^prefix/, the database will use an index when available and appropriate (much like most SQL databases that use indexes for a LIKE 'prefix%' expression). This only works if you don't have i (case-insensitivity) in the flags.

とあり、simple prefix queries (also called rooted regexps) like /^prefix/ のような場合を除きインデックスは使われない(使えない)ので、:case_sensitive => false を指定した場合、

validates_uniqueness_of は validation に際して、フィールドの値がユニークであることを保証するために、$regex を用いたクエリでコレクションの全ドキュメント(またはそのインデックス上のエントリ)をスキャンすることになる。*1


ということで注意が必要というか、validates_uniqueness_of を使うなら最初から大文字の使用を禁じるとか、大文字は小文字に変換した上で格納するなどして、インデックスを効かせられるようにしておくべき。

*1:対象フィールドが主キーの場合は確かインデックス上のエントリのスキャンだけで済むはずだ条件によってはインデックス上のエントリのスキャンだけで済むこともあった気がするが、ドキュメントの数だけ正規表現マッチが必要になる点は変わらない。

word の中の単語を選択する textobj-wiw を書いた

(この記事は Vim Advent Calendar 2011 5日目の記事です。前日は thincaさんでした!)


前々から、地味にフラストレーションを感じていたこととして、

この状態から、"word" の部分を書き換えたい場合、 

this_is_a_word_in_a_very_long_identifier

 ↓

this_is_a_word_in_a_very_long_identifier

こう選択する text-object が欲しいんだよ! というのがありました。


また、選択と同様に、

ここから、

this_is_a_word_in_a_very_long_identifier

 ↓

this_is_a_word_in_a_very_long_identifier
this_is_a_word_in_a_very_long_identifier
this_is_a_word_in_a_very_long_identifier

こんな具合にジャンプしていきたいんだよ! とか。


で、この手の「word の中の単語」は上に挙げたような snake case の中だけでなく、thisIsAWordInAVeryLongIdentifier のような camel case の中にもあるし、this#is#a#word#in#a#very#long#identifier のような、Vim script のオートロード関数の名前の中にもあります。

それら「word の中の単語」を一様に取り扱える text-object があれば便利だと思い、kanaさんの textobj-user を使って textobj-wiw というのを書いてみました。

wiw は word in word の略です。「word の中の単語」とは要するに、「Vim における word に含まれている、人間が単語と認識している部分」のことです。

これを使うと、「word の中の単語」間をジャンプで移動できたり、おなじみの iw, aw と同様の操作で「word の中の単語」をサクッと編集/削除/選択できるようになります。

インストール

プラグインをインストールするいつもの方法で

の2つをインストールして下さい。

説明になってないような気もしますが、重要なことは textobj-user が必要だということです。

キーマッピング

textobj-wiw には w, b, e, ge, iw, aw に相当する一連のキーマッピングがあり、デフォルトでは以下のキーマッピングを定義するようになっています。

        lhs     Modes   rhs
        ---------------------------------------
        ,w      nxo     (textobj-wiw-n)
        ,b      nxo     (textobj-wiw-p)
        ,e      nxo     (textobj-wiw-N)
        ,ge     nxo     (textobj-wiw-P)

        a,w     xo      (textobj-wiw-a)
        i,w     xo      (textobj-wiw-i)

対応する動作がわかりやすいように prefix を付ける方式にしました。prefix は g:textobj_wiw_default_key_mappings_prefix変数で設定できます。(デフォルトは ",")

デフォルトのキーマッピング体系が気に入らない場合は、vimrc にて g:textobj_wiw_no_default_key_mappings 変数を定義すればデフォルトキーマッピングの定義をキャンセルできます。その上で、各自でよさげなキーマッピングを定義して下さい。

似ている text-objects

キャメルケースについては既にやっている方がいました。あわわ。

また、ある特定の文字で囲まれた部分、を取り扱う text-object としては thincaさんの textobj-between もあります。こちらは選択範囲が word内に限定されません。

探せば他にもあるような気がしますが、恐いのでこれ以上は探しませんw


それでは皆さん、Happy Vimmer days!

unite-outline の近況: Ruby と RSpec の見出し抽出を改善

今秋から Rails やってます。というわけで、unite-outline の Rubyサポートを強化中。まずは、Ruby用 outline info の改善から。

def 以外のメソッド定義も拾う

なんと、メソッドの定義は def しか拾ってなかったので、attr_accessor, attr_reader, attr_writer, alias などが今まで見出しになっていなかったという驚愕の事実。おいw

こいつらもメソッドの定義だから見出しになるべき、ということで、そのように修正。

Rails だと mattr_ とか cattr_ とかもあるよねー、ということでそっちにも対応。

正しい構造のツリー

Ruby(というか Rails界隈?)では protected, private なメソッドを public なものより一段字下げするという慣習で書かれているコードが少なからずあって、Ruby ではインデントの深さを元に見出しレベルを決めている関係上、一連の privateメソッドが直前の publicメソッドの小見出しになるということが起こっていた。(:help unite-outline-known-issues を参照)

で、これはちょっとかっこ悪いというんで、protected または private の後で字下げしている場合には、字下げされているメソッドを protected または private の下に束ねて、直前の publicメソッドの小見出しにならないようにした。

Before:

▲ privateメソッドが publicメソッドの小見出しになってしまっている。

 ↓

After:

▲ private を見出しにすることで回避

これで絞り込みの結果に無関係なメソッドが残ることはなくなった。

DSL対応

後、ftdetect で複合ファイルタイプの設定をしなくても DSL用の outline info へ処理を委譲できる仕組みを実装した。これにより、ファイルタイプが ruby のままでもファイル名が *_spec.rb なら RSpec用の見出し抽出を行う、みたいなことができるようになった。


▲ describe, it などが見出しになっているが、実はファイルタイプはただの ruby

具体的には、オートロード関数 path#to#outline#{filetype}#outline_info() にて、呼び出し側から渡される context の情報を元に、別のファイルタイプ名(例えば 'ruby/rspec' とか)を文字列として返すと、そっちへ処理が委譲される。(ファイルタイプ・リダイレクトと命名

Ruby用の outline info ではこの仕組みを使い、ファイル名が *_spec.rb なら RSpec用の outline info へ処理をリダイレクトさせている。

この仕組みを使い、Ruby の主要な DSL に順次対応していこうと思っている。とりあえず次のターゲットは Rake あたり。

unite-outline の近況: C のマクロがどう展開されるかを表示するようにした

C のマクロが展開されてどうなるかを => の後に表示するようにしてみた。

Before:

 ↓
After:

上の画像より空間の利用効率が上がって情報量が増えたのがわかると思う。

do { ... } while(0) のような複数行に渡るものは最初の数行分しか表示できないけど、アウトラインってことならこんなもんで十分だろう。詳細はプレビューするなりジャンプするなりすれば確認できるわけだし。

Railsノート - Thor によるメッセージの色付けをやめさせる

久しぶりに Rails を触る。rails generate とかやるとメッセージに色が付くようになっていた。お、見やすいじゃんと思ったのも束の間、端末エミュレータの背景色と同化して一部の文字が見えていないことが判明。

調べると、rails generate は Thor というライブラリを叩いていて、これがメッセージの着色もやっている模様。

で、案の定、文字色の指定はハードコードされていて、こちらで使っている端末エミュレータの色設定などお構いなしだった。もー、赤と緑だけ使ってくれればいいものを、なんでそう多彩な色を使おうとするかなー。背景色は黒とは限らないんだぞー。

なんとかならないもんかと Thor のコードを見ていたら、環境変数 THOR_SHELL を "Basic" に設定すれば色付けなしにできることを発見↓

# thor-0.14.6/lib/thor/shell.rb

class Thor
  module Base
    # Returns the shell used in all Thor classes. If you are in a Unix platform
    # it will use a colored log, otherwise it will use a basic one without color.
    #
    def self.shell
      @shell ||= if ENV['THOR_SHELL'] && ENV['THOR_SHELL'].size > 0
        Thor::Shell.const_get(ENV['THOR_SHELL'])
      elsif RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
        Thor::Shell::Basic
      else
        Thor::Shell::Color
      end
    end

というわけで、zshrc にて

export THOR_SHELL=Basic

とやって、無事にメッセージが読めるようになった。やれやれ。*1

その後さらに調べると

config/application.rb にて

    config.generators.colorize_logging = false

と設定することでも同じ効果が得られることがわかった。

ただ、この方法だとアプリケーションを新規に作るたびに設定しなければならないし、「自分の端末の色設定では色付けされた文字が見にくい!」というような理由でアプリケーションの設定をいじるのもどうかと思う*2ので、この場合*3環境変数 THOR_SHELL の設定で対応すべきだろう。

*1:上のコードを見る限りでは、自分でサブクラスを書いてそれを使わせることもできるようだが、さすがにそこまでやりたくない。

*2:開発環境に関する設定をアプリケーション側で行うべきではないだろう。

*3:自分の端末の色設定の都合で色付けを抑制したい場合

alignta 秋の仕様改訂、の近況

とりあえずここまでの作業分を master へマージしました。

でやろうって言った変更と、

にあるもののうち、「繰り返し」と「履歴機能」以外の部分。

書式が変更になる部分では原則 version 0.2.1 以前のそれも引き続き使えるようになっているので、大きな混乱は起きないと思いますが、古い書式は将来的には使えなくする予定*1なので、preset arguments や preset options などに古い書式が残っている場合は、今からでも新しい書式に書き換えておくと吉です。

具体的には、

  • :Alignta! は :Alignta に変える。
    • ! の有無は今や意味を持たない。
  • 整列回数指定は pattern/number または pattern/g になったので変える。
    • pattern{1} → pattern/1
    • pattern{+} → pattern/g
  • マージン指定の @ は要らなくなったので取る。


alignta の書式が変わるのはおそらく*2これが最後になると思います。*3書式の改善はこれで一段落し、今後は「繰り返し」や「履歴機能」といった機能の追加が主になります。*4

*1:残しておいてもいいのですが、パターンと衝突した場合に、「なぜ?」となりそうなので、将来的にはなくしたい。

*2:ちょっと弱気w しかし世の中何が起こるかわからないからなあ。

*3:実のところ、こういった書式の変更をある程度見越していたので、これまで alignta を大っぴらには喧伝してこなかったというのもある。

*4:年内をめどにぼちぼちやっていこうかと。