ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja nb ja ja ja ja uz io ja en ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ko ja ko ja ko ja ja ja ja ko ja ja ja ja ja ja ja ko ja ja ja ko ja ja ja ja ja ja ja ja ja ja ja ja ko ja io bg ja ja es en fi en uk pt sv uk en ja ja ja es da pa da wuu ro ja ja ja ja ja ja es en fi en uk pt sv uk en ja ja ja ja ja es en fi en en uk pt en sv uk en en ja ja es en fi en en uk pt en ja ja ja es da pa da wuu ro ja ja ja ja ja ja ja es en fi en uk pt sv uk en ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
{ "a": 1, "a": "b" }
The names within an object SHOULD be unique.
指定したjsonb型のデータを含んでいるレコードを検索
検索対象のJSONとして次の2つのJSONがあるとします。
{"a": "hello", "b": "world"} {"a": "hello", "c": "world"}
クエリーが次の場合は両方ヒットします。どちらもトップのオブジェクトに「a」というキーがあり、その値が「"hello"」だからです。
{"a": "hello"}
クエリーが次の場合は「{"a": "hello", "b": "world"}」だけヒットします。もう片方はトップのオブジェクトに「b」というキーが存在しないからです。
{"b": "world"}
指定したキーの値に対する完全一致検索
この検索は「指定したjsonb型のデータを含んでいるレコードを検索」のサブセットなので省略します。
指定したキーの値に対する範囲検索(a)
検索対象のJSONとして次の2つのJSONがあるとします。
{"code": 200} {"code": 300}
「code」の値が200番台という条件で検索できるということです。「200 <= code < 300」という条件で検索できるイメージです。この場合は{"code": 200}だけがヒットします。
指定したキーの値に対する全文検索(a)(b)
検索対象のJSONとして次の2つのJSONがあるとします。
{"message": "サーバーが起動しました"} {"message": "サーバーがダウンしました"}
「message」の値に「起動」というテキストを含むかという条件で検索できるということです。この場合は{"message": "サーバーが起動しました"}だけがヒットします。
全部の値に対する完全一致検索
検索対象のJSONとして次の2つのJSONがあるとします。
{"code": 200} {"codes": [200]}
JSONのどこかに「200」という数値があるかという条件で検索できるということです。この場合はどちらにもヒットします。「200」がどこにあるかは問わないからです。
全部の値に対する範囲検索(a)
検索対象のJSONとして次の2つのJSONがあるとします。
{"code": 200} {"codes": [299]}
JSONのどこかに200以上300未満という数値があるかという条件で検索できるということです。この場合はどちらにもヒットします。最初のJSONには「200」があり、2つめのJSONには「299」があるからです。
全部の値に対する全文検索(a)(b)
検索対象のJSONとして次の2つのJSONがあるとします。
{"message": "サーバーが起動しました"} {"tags": ["起動失敗"]}
JSONのどこかに「起動」というテキストを含む値があるかという条件で検索できるということです。この場合はどちらもヒットします。最初のJSONは「"起動しました"」に「起動」が含まれていて、2つめのJSONは「"起動失敗"」に「起動」が含まれているからです。
だいぶ前置きが長くなりましたが、このような検索をインデックスを使って高速に実現するためにどのように実装しているかを説明します。
ポイントは次の2点です。
JSONの値をそれぞれ1つのデータとして分割する
内部で複数のインデックスを組み合わせる
まず、それぞれ1つのデータとして分割するということについて説明します。
次のJSONを考えます。
{"a": "hello", "b": "world"}
このJSONには「"hello"」と「"world"」という2つの値があるので、次のように2つのデータに分割します。A → Bという書き方は「Aというパスの値はBだよ」ということを示したつもりです。
.a → "hello"
.b → "world"
Groongaにはストレージ機能があり、RDBMSのようにテーブルを作り、そこにレコードを保存することができます。PGroongaはそれぞれの値を1つのレコードとして保存します。このレコードを保存しているテーブルを「Valuesテーブル」と呼んでいます。
今回の例だと次のようなレコードを保存するということです。
これだけだとJSONの値は見つけられてもその値を持っている(PostgreSQLの)レコードを見つけることができません。そのため、もう1つテーブルを作ります。「Sourcesテーブル」と呼んでいるもので、インデックス対象のデータを管理しています。このテーブルのレコードがインデックス対象のレコードに対応します。このSourcesテーブルの各レコードにValuesテーブルへの参照を格納します。元のJSONがどの値から成り立っていたかを示しています。
なお、Valuesテーブルへの参照にはGroongaのベクター型のカラムを使います。これは、PostgreSQLでいう配列型です。
今回の例では次のようなレコードを保存するということです。valuesカラムの中に入っている1も2も前述のValuesテーブルのレコードのIDです。(ctidというのはPostgreSQL特有のIDでデータの実体を識別するIDのようなものだと思ってください。)
あとは、次のような流れで検索できるようにするだけです。
Valuesテーブルから検索(たとえば、「.aカラムの値が"hello"」という完全一致検索)
1.で見つかったレコードをvaluesカラムに持つレコードをSourcesテーブルから検索
2.で見つかったレコードからctidカラムの値を参照し、PostgreSQLのレコードを特定する
この、1.と2.を高速に実現するために「内部で複数のインデックスを組み合わせる」ということをしています。
まず、Valuesテーブルを考えます。
「.aカラムの値が"hello"」という完全一致検索をするためには「path == "." && string == "hello"」という条件を評価すればよいことになります。そして、これを高速に実現するためにpathとstringにインデックスを用意します。
これで、「1. Valuesテーブルから検索」を高速に実現できるようになります。
次に、「2. 1.で見つかったレコードをvaluesカラムに持つレコードをSourcesテーブルから検索」を高速に実現するためのインデックスを用意します。
これは、Valuesテーブルにインデックスカラムindexを追加します。indexにはこのレコードを参照しているSourcesテーブルのレコードのIDのリストが入ります。(インデックスカラムは転置インデックスでレコードのIDのリストはポスティングリストだと言えばわかる人にはわかります。)
今の例ではどちらの値もSourcesテーブルのID100のレコードから参照されているので、どちらも[100]になります。
この情報があると、「2. 1.で見つかったレコードをvaluesカラムに持つレコードをSourcesテーブルから検索」を高速に実現できます。なぜなら、indexカラムに入っているレコードIDのリストが求めるものだからです。
今の例ではValuesテーブルの次のレコードがヒットしています。
このレコードを含んでいるSourcesテーブルのレコードは100だけです。なぜなら、indexに100だけが入っているからです。
Sourcesテーブルのレコードが見つかったら「3. 2.で見つかったレコードからctidカラムの値を参照し、PostgreSQLのレコードを特定する」は簡単です。ctidカラムの値を返すだけだからです。
今の例だと[1,1]を返すだけです。
このように、次の2つを組み合わせてjsonb型の値を高速に実現できるようにしています。
JSONの値をそれぞれ1つのデータとして分割する
内部で複数のインデックスを組み合わせる
この実現方法の利点を説明します。
この実現方法ではJSONそのものをそのまま検索しません。JSON内のそれぞれの値を別のレコードに分割し、それぞれの値に絞って検索します。これが範囲検索や全文検索といった高度な検索を使える理由です。
たとえば、先ほどの例では次のように値を保存しています。
JSONは構造がネストしているため、条件を指定することが困難ですが、これは表になっているので、path == ".a" && string == "hello"と条件を指定できます。演算子を変えてstring @ "hello"とすれば簡単に全文検索をすることができます。(Groongaでは@は全文検索の演算子です。)
数値ならnumber > 100 && number < 200とすれば範囲検索になります。
また、単なる表なので、いつも通りインデックスを用意できます。操作に合わせたインデックスを作ればその操作を高速に実現できるというわけです。たとえば、文字列の前方一致検索をできるインデックスを用意すれば高速に文字列を前方一致検索できます。
この「表である」ということには別の利点もあります。JSONのどこにある値か関係なく検索できるという点です。
「.aにある"hello"という値」という条件は「path == ".a" && string == "hello"」となりますが、pathを条件に加えなければ「どこにあってもいいけど"hello"という値」という条件「string == "hello"」となります。
JSONで値を格納したいということは、データの構造がブレることがある場合が多いでしょう。そのとき、「どこのキーかは問わないけど○○というキーワードを含んでいるJSONを全文検索」とできたら便利ではないでしょうか?
これもpathを条件に含めずに「string @ "○○"」とすれば実現できます。
この実現方法の欠点はディスク使用量が多いことです。
値を分解してGroongaに保存しているので、PostgreSQLが持っているデータを2重に持っています。
また、1つのJSONには複数の値があるので、Valuesテーブルのレコード数が多くなります。これもデータが増える原因になります。
ただし、少しでもValuesテーブルのサイズを小さくする工夫はしています。違うJSONの中に、同じパスで同じ値がある場合は同じValuesレコードを共有します。多くの場合、ある程度同じ構造や同じ値がでてくると予想していて、この工夫は効果があると期待しています。
たとえば、Webサーバーのログを考えます。次のようなJSONがたくさん保存されることでしょう。
{ "code": 200, "path": "/" } { "code": 200, "path": "/favicon.png" }
.codeに注目してください。値がすべて200です。HTTPのステータスコードは200や404や500などよく使われる値に偏ります。同じパスで同じ値のときにValuesレコードを共有すると.codeが200という値は共有できます。このようにValuesテーブルの肥大化を防ぐ工夫をしています。
説明を単純にするために文字列型の値だけを例にして説明しました。JSONでは文字列型以外に数値型と真偽値型があります。それらをどう扱っているかの説明は省略しました。
また、JSON内の配列をどう扱っているか、パスをより簡単に指定するための工夫も省略しました。
実はpathカラムにはもう少し違う形でパスが入っているということの説明も省略しました。
速度面についても説明していません。
使い方(SQLの書き方)についても説明していません。
PGroongaは0.9.0でjsonb型の検索をサポートしました。そして、その実現方法を説明しました。
興味がでてきた方はドキュメントを参考にして使ってみてください。使い方(SQLの書き方)を説明しています。
わからないことがあったらGitHubのissueやメーリングリスト、チャットで質問してください。
ある程度大きなデータで性能を測定し、それを公開してくれると大変励みになるのでよろしくおねがいします。
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
クリアコードとフリーソフトウェアとビジネス
Firefox 38以降で急にSSL/TLSのエラーが出るようになった、という問題の原因と対策
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。