jsqueryを使ってみた

まえがき

みなさん、こんにちは。ぬこ@横浜です。

このエントリはPostgreSQL Advent Calendar 2014 - Qiitaの11日目のエントリです。
昨日は s87さんに書いていただきました。

今回、PostgreSQL Advent Calendar 2014 - Qiitaがなかなか埋まってなくて(今日見たらほぼ埋まってましたが)、11日目が空いたままになっていたので、急遽、不肖ぬこ@横浜が書くことにしました。

今日のテーマ

先日のPostgreSQLカンファレンス2014 | 日本PostgreSQLユーザ会で、JSONBについてちょこっと喋らせていただきました(JSONB データ型を 使ってみよう)。
また、7日にはecpgからJSONBを扱うという誰得なエントリを書いてみました。
で、今日のテーマは・・・やっぱりJSONBに関するネタです。ホントは12/20日用にとっておこうかと思ってたけどw

jsqueryとは

jsqueryとは・・・私もまだ読み解いている途中なんですが、JSONBに対して強力なクエリ機能を提供する拡張機能です。
JSONBに対する様々な便利な演算子や、GINインデックス用の演算子などをサポートしています。
今年の9月11日にSRA OSS.さんが主催した、PostgreSQL 9.4 最新情報セミナー 〜 新しいJSONデータ型開発者の Oleg 氏を迎えて 〜が参考になるかと思います。

開発しているのはJSONBの開発チーム。PostgreSQL 9.4には含まれていませんが、もしかするとPostgreSQL 9.5には入るかもしれません。
現状、ソースなどはhttps://github.com/akorotkov/jsqueryから取得できるっぽい。

インストールとビルド

まずは、jsqueryモジュールをとりあえず使えるようにしてみましょうか。

ソースの入手

今のところ、開発中なのでRPMパッケージ等はなさそうです。あっても自分は基本、野良ビルド派なので使いませんが。
ということで、ソースを入手してビルドしてみますか。
PostgreSQL 9.4rc1のソースアーカイブを展開したディレクトリに移動します。

ここで、jsqueryのgitリポジトリ(https://github.com/akorotkov/jsquery.git)からソースを入手します。

[nuko@localhost postgresql-9.4beta2]$ git clone https://github.com/akorotkov/jsquery.git
Cloning into 'jsquery'...
remote: Counting objects: 451, done.
remote: Total 451 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (451/451), 249.36 KiB | 154.00 KiB/s, done.
Resolving deltas: 100% (286/286), done.
[nuko@localhost postgresql-9.4beta2]$
ソースのビルドとインストール

jsqueryという名前をフォルダが生成されているので、jsqueryフォルダに移動し、USE_PGXS=1オプションを付与してmakeとmake installします。

[nuko@localhost postgresql-9.4beta2]$ cd jsquery/
[nuko@localhost jsquery]$ make USE_PGXS=1
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -I. -I./ -I/home/nuko/pgsql-9.4-b2/include/server -I/home/nuko/pgsql-9.4-b2/include/internal -D_GNU_SOURCE   -c -o jsonb_gin_ops.o jsonb_gin_ops.c
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -I. -I./ -I/home/nuko/pgsql-9.4-b2/include/server -I/home/nuko/pgsql-9.4-b2/include/internal -D_GNU_SOURCE   -c -o jsquery_constr.o jsquery_constr.c
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -I. -I./ -I/home/nuko/pgsql-9.4-b2/include/server -I/home/nuko/pgsql-9.4-b2/include/internal -D_GNU_SOURCE   -c -o jsquery_extract.o jsquery_extract.c
/usr/bin/bison -d -o jsquery_gram.c jsquery_gram.y
/usr/bin/flex   -o'jsquery_scan.c' jsquery_scan.l
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -I. -I./ -I/home/nuko/pgsql-9.4-b2/include/server -I/home/nuko/pgsql-9.4-b2/include/internal -D_GNU_SOURCE   -c -o jsquery_gram.o jsquery_gram.c
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -I. -I./ -I/home/nuko/pgsql-9.4-b2/include/server -I/home/nuko/pgsql-9.4-b2/include/internal -D_GNU_SOURCE   -c -o jsquery_io.o jsquery_io.c
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -I. -I./ -I/home/nuko/pgsql-9.4-b2/include/server -I/home/nuko/pgsql-9.4-b2/include/internal -D_GNU_SOURCE   -c -o jsquery_op.o jsquery_op.c
jsquery_op.c: 関数 ‘recursiveExecute’ 内:
jsquery_op.c:364:13: 警告: 変数 ‘r’ が設定されましたが使用されていません [-Wunused-but-set-variable]
     int32   r;
             ^
jsquery_op.c:434:13: 警告: 変数 ‘r’ が設定されましたが使用されていません [-Wunused-but-set-variable]
     int32   r;
             ^
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -I. -I./ -I/home/nuko/pgsql-9.4-b2/include/server -I/home/nuko/pgsql-9.4-b2/include/internal -D_GNU_SOURCE   -c -o jsquery_support.o jsquery_support.c
gcc -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -fpic -shared -o jsquery.so jsonb_gin_ops.o jsquery_constr.o jsquery_extract.o jsquery_gram.o jsquery_io.o jsquery_op.o jsquery_support.o -L/home/nuko/pgsql-9.4-b2/lib -Wl,--as-needed -Wl,-rpath,'/home/nuko/pgsql-9.4-b2/lib',--enable-new-dtags  
[nuko@localhost jsquery]$ make USE_PGXS=1 install
/usr/bin/mkdir -p '/home/nuko/pgsql-9.4-b2/lib'
/usr/bin/mkdir -p '/home/nuko/pgsql-9.4-b2/share/extension'
/usr/bin/mkdir -p '/home/nuko/pgsql-9.4-b2/share/extension'
/usr/bin/install -c -m 755  jsquery.so '/home/nuko/pgsql-9.4-b2/lib/jsquery.so'
/usr/bin/install -c -m 644 jsquery.control '/home/nuko/pgsql-9.4-b2/share/extension/'
/usr/bin/install -c -m 644 jsquery--1.0.sql '/home/nuko/pgsql-9.4-b2/share/extension/'
[nuko@localhost jsquery]$ 

途中で、警告が2つほど出てますが、まあ今回はさらっと無視しますw
生成されるEXTENSION名は jsquery です。これを利用したいデータベースに登録します。

[nuko@localhost jsquery]$ psql jsonb
psql (9.4beta2)
Type "help" for help.

jsonb=# CREATE EXTENSION jsquery ;
CREATE EXTENSION
jsonb=# 

無事に登録できました!

登録されたjsqueryの中身。

登録された中身を見てみましょう。

jsonb=# \dx
                  List of installed extensions
  Name   | Version |   Schema   |          Description           
---------+---------+------------+--------------------------------
 jsquery | 1.0     | public     | data type for jsonb inspection
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)

jsonb=# \dx+ jsquery 
                                          Objects in extension "jsquery"
                                                Object Description                                                
------------------------------------------------------------------------------------------------------------------
 function gin_compare_jsonb_path_value(bytea,bytea)
 function gin_compare_jsonb_value_path(bytea,bytea)
 function gin_compare_partial_jsonb_path_value(bytea,bytea,smallint,internal)
 function gin_compare_partial_jsonb_value_path(bytea,bytea,smallint,internal)
 function gin_consistent_jsonb_path_value(internal,smallint,anyarray,integer,internal,internal,internal,internal)
 function gin_consistent_jsonb_value_path(internal,smallint,anyarray,integer,internal,internal,internal,internal)
 function gin_debug_query_path_value(jsquery)
 function gin_debug_query_value_path(jsquery)
 function gin_extract_jsonb_path_value(internal,internal,internal)
 function gin_extract_jsonb_query_path_value(anyarray,internal,smallint,internal,internal,internal,internal)
 function gin_extract_jsonb_query_value_path(anyarray,internal,smallint,internal,internal,internal,internal)
 function gin_extract_jsonb_value_path(internal,internal,internal)
 function gin_triconsistent_jsonb_path_value(internal,smallint,anyarray,integer,internal,internal,internal)
 function gin_triconsistent_jsonb_value_path(internal,smallint,anyarray,integer,internal,internal,internal)
 function json_jsquery_exec(jsonb,jsquery)
 function jsquery_cmp(jsquery,jsquery)
 function jsquery_eq(jsquery,jsquery)
 function jsquery_ge(jsquery,jsquery)
 function jsquery_gt(jsquery,jsquery)
 function jsquery_hash(jsquery)
 function jsquery_in(cstring)
 function jsquery_join_and(jsquery,jsquery)
 function jsquery_join_or(jsquery,jsquery)
 function jsquery_json_exec(jsquery,jsonb)
 function jsquery_le(jsquery,jsquery)
 function jsquery_lt(jsquery,jsquery)
 function jsquery_ne(jsquery,jsquery)
 function jsquery_not(jsquery)
 function jsquery_out(jsquery)
 operator !(NONE,jsquery)
 operator &(jsquery,jsquery)
 operator <(jsquery,jsquery)
 operator <=(jsquery,jsquery)
 operator <>(jsquery,jsquery)
 operator =(jsquery,jsquery)
 operator >(jsquery,jsquery)
 operator >=(jsquery,jsquery)
 operator @@(jsonb,jsquery)
 operator @@(jsquery,jsonb)
 operator class jsonb_path_value_ops for access method gin
 operator class jsonb_value_path_ops for access method gin
 operator class jsquery_ops for access method btree
 operator class jsquery_ops for access method hash
 operator family jsonb_path_value_ops for access method gin
 operator family jsonb_value_path_ops for access method gin
 operator family jsquery_ops for access method btree
 operator family jsquery_ops for access method hash
 operator |(jsquery,jsquery)
 type jsquery
(49 rows)

jsonb=# 

いろんな関数、オペレータが追加されてますねー。GINアクセスメソッドも追加されているので、GINインデックス構築時にはこの新しいオペレータを指定するのかも。
あと、最後にも表示されてますが、jsqueryというのはパッケージ名だけでなく型名としても使われています。
この型名、やっとで意味がわかりました。
jsqueryは"Jsonb Search QUERY "の略なんですね。textsearch用の型がtsqueryなのと同じ考え方(Text Search QUERY)で略された名前なのか。
そういえば全文検索(tsquery)もGIN, JSONBと同じOlegさんたちの開発チームが作ったんだっけ。

でも、jsqueryという名前のググらビリティって微妙・・・。
jqueryの間違いじゃね?とグーグル先生には思われてるみたい。(´・ω・`)

jsqueryを使ってみる。

登録できたので、早速使ってみましょう。

jsqueryはjsob型に対して @@ という演算子とJSONBのクエリ文字列を渡します。

jsonb @@ jsquery

このクエリ文字列の構文形式も、結構複雑というかバリエーションがあって一度に紹介しきれないのですが、今回はそのうち、代表的なものや「これは便利そう!」というものを紹介したいと思います。

キーの存在有無

9.4までのJSONB演算子には、キーの存在有無の真偽値を返すものがなかったのですが、jsqueryではこれに対応しています。

jsonb=# SELECT '{"id": 3, "name": {"nickname": "nuko", "distribute": ["ksj", "neo4jfdw"]},"age":17}'::jsonb @@ 'name = *';
 ?column? 
----------
 t
(1 row)

jsonb=# SELECT '{"id": 3, "name": {"nickname": "nuko", "distribute": ["ksj", "neo4jfdw"]},"age":17}'::jsonb @@ 'hoge = *';
 ?column? 
----------
 f
(1 row)

jsonb=# 

この例では、nameというキーは存在するのでtrueを、hogeというキーは存在しないのでfalseを返却します。

階層の記述をスキップしたキーへの条件指定

9.4までのJSON演算子にもパス指定の演算子( #>, #>> )はありましたが、最上位の階層のキーから末端までの全てのパスを記述する必要がありました。

jsonb=# SELECT ('{"a1":{"b1":100, "b2":{"c1":50, "c2":200}},"a2":-10}'::jsonb #>> '{a1,b2,c2}') = '200';
 ?column? 
----------
 t
(1 row)

jsqueryでも勿論、フルパス的な記述は可能ですが

jsonb=# SELECT '{"a1":{"b1":100, "b2":{"c1":50, "c2":200}},"a2":-10}'::jsonb @@ 'a1.b2.c2 = 200';
 ?column? 
----------
 t
(1 row)

以下のように "*.キー名" みたいな記述で、末端階層のパスを指定することができます。

jsonb=# SELECT '{"a1":{"b1":100, "b2":{"c1":50, "c2":200}},"a2":-10}'::jsonb @@ '*.c2 = 200';
 ?column? 
----------
 t
(1 row)

jsonb=# SELECT '{"a1":{"b1":100, "b2":{"c1":50, "c2":200}},"a2":-10}'::jsonb @@ '*.c2 = 100';
 ?column? 
----------
 f
(1 row)

jsonb=# 

XMLxpathを知ってる人だと、"//要素名" っぽい感じだと思ってもらえばいいかも。
ただ、条件式としてはつけるけど、値を取得しようとして以下の様な書き方をすると怒られる。(´・ω・`)

jsonb=# SELECT '{"a1":{"b1":100, "b2":{"c1":50, "c2":200}},"a2":-10}'::jsonb @@ '*.c2';
ERROR:  bad jsquery representation at character 73
DETAIL:  syntax error, unexpected $end at end of input
STATEMENT:  SELECT '{"a1":{"b1":100, "b2":{"c1":50, "c2":200}},"a2":-10}'::jsonb @@ '*.c2';
ERROR:  bad jsquery representation
LINE 1: ...:100, "b2":{"c1":50, "c2":200}},"a2":-10}'::jsonb @@ '*.c2';
                                                                ^
DETAIL:  syntax error, unexpected $end at end of input
jsonb=# 

数値をきちんと扱える

JSONB型自体は、内部格納形式として数字はnumericとして格納してますが、9.4までのJSONB演算子(->>, #>>)で取り出したときには文字列になってしまいます。
なので、数字を含むJSONBを扱う場合には、CASTするなどの一工夫が必要でした。

jsonb=# SELECT data FROM test WHERE data @@ 'age >=17';
                                        data                                        
------------------------------------------------------------------------------------
 {"id": 2, "age": 59, "name": {"last": "Lane", "first": "Tom"}}
 {"id": 3, "age": 17, "name": {"first": "nuko"}, "distribute": ["ksj", "neo4jfdw"]}
(2 rows)

jsqueryではこんな感じで書きます。

jsonb=# SELECT data FROM test WHERE data @@ 'age >= 17';
                                        data                                        
------------------------------------------------------------------------------------
 {"id": 2, "age": 59, "name": {"last": "Lane", "first": "Tom"}}
 {"id": 3, "age": 17, "name": {"first": "nuko"}, "distribute": ["ksj", "neo4jfdw"]}
(2 rows)

配列に対する評価

指定した値が、配列に含まれるかどうかを評価する演算子(#)もあります。

jsonb=# SELECT data FROM test WHERE data @@ 'distribute.# = ksj';
                                        data                                        
------------------------------------------------------------------------------------
 {"id": 3, "age": 17, "name": {"first": "nuko"}, "distribute": ["ksj", "neo4jfdw"]}
(1 row)

配列同士を比較してオーバラップしているか判定する演算子(&&)や、

jsonb=# SELECT '{"a":[1, 10, 100, 100]}'::jsonb @@ 'a && [20,100]';
 ?column? 
----------
 t
(1 row)

jsonb=# SELECT '{"a":[1, 10, 100, 100]}'::jsonb @@ 'a && [2,200]';
 ?column? 
----------
 f
(1 row)

jsonb=# 

配列が包含されているか判定する演算子(@>)もあります。

jsonb=# SELECT '{"a":[1, 10, 100, 100]}'::jsonb @@ 'a @> [10,100]';
 ?column? 
----------
 t
(1 row)

jsonb=# SELECT '{"a":[1, 10, 100, 100]}'::jsonb @@ 'a @> [10,200]';
 ?column? 
----------
 f
(1 row)

jsonb=# 

全部の演算子について紹介したわけではないですが、ここで紹介したものだけでも、JSONBを使う場合には結構便利になりそうなものが多いですね。

まだ不安定な部分も・・・

jsquery、なかなか楽しい機能も入っていてとっても期待できますが、現状の版だとちょっと間違ったjsqueryクエリを記述すると、わりと簡単に落ちます(´・ω・`)

jsonb=# SELECT data @@ '//first' FROM jsonb_t;
server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
!>

無論、このへんは今後、改善されていくんじゃないかとは思いますが・・・。


明日は・・・osaponさんですね。よろしくお願いします!