Solr の block join を調べてみたけど、ちょっと違ったという話。
Twitter でこんな投稿をしていたのですが。Elasticsearch のネストの話で、Elasticsearch のドキュメントの定義は、従来の検索エンジンと異なり、ドキュメントやフィールドをネストでき、子ドキュメントだとか子フィールドを持つことができます。
まぢか、すげぇ! / “内部では入れ子ドキュメントは同じインデックス内部に格納されあた分離されたインデックス文書として索引付けされる。ElasticSearchはそれらが高速なJOIN命令を用いて取得することが可能な方法で索引 http://t.co/yy2bPaB91a
— よしだ (@yoshi0309) 2014, 9月 24
何がすごいって、1.かなり複雑な入れ子も表現できる点、2.さらに個別に部分更新ができる点、3.内部でのjoinなど透過的に扱える点。パフォーマンスが気になるけど・・。
— よしだ (@yoshi0309) 2014, 9月 24
上のつぶやきは、もともとは Solr の ドキュメントのネストを調べていて、あまりイケていなかったことに端を発するわけなのですが・・・。とにかくイケてないw
というわけで、Solr のネストこと、Block Join のお話。
従来のフラットなデータ構造のデメリット
RDB は R とつくだけあって、複数テーブルを作成して外部キーを設定して、Join して使うのが普通です。一方、検索エンジンはフラットなデータ構造しか持つことが出来ません*1。なので、データを入れるときにフラット化する処理が必要で、例えば、DB のデータを検索エンジンに入れるときは、必要なテーブルを全てJOINしたselect文でデータを取得して検索エンジンに入れます。
この方法の最大のデメリットは、子テーブルのレコード1つが更新されるとテーブル間の多重度によっていは、一度に複数の検索インデックスの更新が必要になります。例えば、商品情報テーブルに対して、商品の分類情報をもつ商品カテゴリマスタテーブルを JOIN して取っていたりすると、マスタのカテゴリ名を変えると、同じカテゴリに入っている商品点数分、全ての検索インデックスの更新が必要になります。
Solrの block join に、このような課題を解決出来ないかなと思って調べだしたのがきっかけです。したがって、block join に求める要件としては以下の2点でした。
- 複数の別のテーブル/ドキュメントを定義しておいて、必要なときに JOIN した形で表示することができる。
- 別々のテーブルのレコードごとに更新することができる。
Solr の BlockJoin
というわけで、Solrのマニュアルから、BlockJoinを試してみました。以下のドキュメントを用意して、curl で登録します。データ登録時の特徴と、注意点は以下のとおりです。
- nest された状態の構造で登録できる
- 以下の状態で登録する場合、_root_ というフィールドが schema.xml に予め必要。_root_には、子ドキュメントには、親ドキュメントの id の値が入る。親ドキュメントには、自分のidが入る。
- 子ドキュメント(以下の場合 id 2 と id 4) で使用するフィールドは、通常のフィールド定義と同じように定義する。ネストだからといって特別な定義は無いが、制限することもできない。例えば、親にしか持たないフィールド、子にしか持たないフィールドのように。
curl "http://localhost:8983/solr/collection1/update?commit=true" -H "Content-Type: text/xml" --data-binary @blockjoindoc.xml
<add> <doc> <field name="id">1</field> <field name="itemname">Solr adds block join support</field> <field name="itemtype">parentDocument</field> <doc> <field name="id">2</field> <field name="description">SolrCloud supports it too!</field> </doc> </doc> <doc> <field name="id">3</field> <field name="itemname">Lucene and Solr 4.5 is out</field> <field name="itemtype">parentDocument</field> <doc> <field name="id">4</field> <field name="description">Lots of new features</field> </doc> </doc> </add>
次に、block join query を利用するとネストした状態で、クエリを投げることができるのですが、ここではいつもどおり、block join query を使用せずに検索してみます。id のみを条件に指定して取ると以下のように、ネストされない状態で取れることがわかります。親と子は別のインデックスになっています。なので、子だけ更新が発生した場合、子のデータのみを更新すれば良さそうです。
{ "responseHeader": { "status": 0, "QTime": 0, "params": { "indent": "true", "q": "id:3", "_": "1411538799725", "wt": "json" } }, "response": { "numFound": 1, "start": 0, "docs": [ { "id": "3", "itemname": "Lucene and Solr 4.5 is out", "itemtype": "parentDocument", "_version_": 1480105312583155700 } ] } }
{ "responseHeader": { "status": 0, "QTime": 1, "params": { "indent": "true", "q": "id:4", "_": "1411538822492", "wt": "json" } }, "response": { "numFound": 1, "start": 0, "docs": [ { "id": "4", "description": "Lots of new features" } ] } }
次に、block join query です。child of で、条件にマッチしたドキュメントの子ドキュメントを取得します。itemtype:parentDocument とは、親ドキュメントを識別するための条件で、必須になります。 itemname:lucene という条件にマッチしたドキュメントの子ドキュメントをとってくるという条件になります。
q={!child of="itemtype:parentDocument"}itemname:lucene
id 3 のドキュメントが itemname:lucene にマッチし、child of なので、その子ドキュメントである id 4 が取れます。
{ "responseHeader": { "status": 0, "QTime": 7, "params": { "indent": "true", "q": "{!child of=\"itemtype:parentDocument\"}itemname:lucene", "_": "1411538563427", "wt": "json" } }, "response": { "numFound": 1, "start": 0, "docs": [ { "id": "4", "description": "Lots of new features" } ] } }
次に、parent which で、条件に一致した親ドキュメントを取得します。
q={!parent which="itemtype:parentDocument"}description:SolrCloud
{ "responseHeader": { "status": 0, "QTime": 3, "params": { "indent": "true", "q": "{!parent which=\"itemtype:parentDocument\"}description:SolrCloud", "_": "1411543351751", "wt": "json" } }, "response": { "numFound": 1, "start": 0, "docs": [ { "id": "1", "itemname": "Solr adds block join support", "itemtype": "parentDocument", "_version_": 1480105312532824000 } ] } }
マルチレベルはサポート外?
子ドキュメントの、さらに子ドキュメントは想定していない模様です。以下のドキュメントを登録してクエリを投げたところ、何故か全件ヒットしました。
<add> <doc> <field name="id">5</field> <field name="itemname">Solr adds block join support</field> <field name="itemtype">parentDocument</field> <doc> <field name="id">6</field> <field name="description">SolrCloud supports it too!</field> <field name="itemtypename">parentDocument</field> <doc> <field name="id">7</field> <field name="description">This is a Pen !</field> </doc> </doc> </doc> </add>
以下は、使用した検索クエリです。
q={!child of="itemtype:parentDocument"}itemname:solr
まとめ:Solr の BlockJoin でできることと、イケてないところ。
というわけで、Solr の BlockJoin では、親と子を紐付けた構造化された形で登録することが出来ます。これによって、条件にマッチするドキュメントの、子ドキュメントや親ドキュメントを取得することが出来ますが、以下の制約があります。
- 親子が紐付いた形で検索結果を取ることが出来ない。
- 複数の親ドキュメントに紐づく子ドキュメントは用意することができない。
- schema.xml のフィールド定義は、親ドキュメントが使うフィールドと、子ドキュメントが使うものを同じように定義する必要があり、親ドキュメントだけが使うフィールドというように制限することは出来ない
- 子ドキュメントのさらに子ドキュメント、といった形で掘ることができない。
補足ですが、今回はドキュメントのネストということで、block join を調べてみましたが、solr には join というクエリが別に存在します。任意のフィールドの値を結合するキーに指定できる点以外は、同じ仕組みを使っているようで、考え方も同じです。
https://wiki.apache.org/solr/Join
じゃあ Elasticsearch は?
最初のツイートに戻ると Elasticsearch がドキュメント構造の柔軟性については良く出来ているかが感じてもらえるかと思います。次は、Elasticsearch を調べてみましょうかね。
まぢか、すげぇ! / “内部では入れ子ドキュメントは同じインデックス内部に格納されあた分離されたインデックス文書として索引付けされる。ElasticSearchはそれらが高速なJOIN命令を用いて取得することが可能な方法で索引 http://t.co/yy2bPaB91a
— よしだ (@yoshi0309) 2014, 9月 24
何がすごいって、1.かなり複雑な入れ子も表現できる点、2.さらに個別に部分更新ができる点、3.内部でのjoinなど透過的に扱える点。パフォーマンスが気になるけど・・。
— よしだ (@yoshi0309) 2014, 9月 24
参考
[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン (Software Design plus)
- 作者: 大谷純,阿部慎一朗,大須賀稔,北野太郎,鈴木教嗣,平賀一昭,株式会社リクルートテクノロジーズ,株式会社ロンウイット
- 出版社/メーカー: 技術評論社
- 発売日: 2013/11/29
- メディア: 大型本
- この商品を含むブログ (6件) を見る
*1:Elasticsearch も Solr も本質的には同じです。ガワでJOINしているように見せかけているだけで、実体はフラットなのは変わりません