はじめに
以前書いた「Solrで入れ子構造の文書をインデックスする」という記事で対象としていたのはSolr 7.5でしたが、現時点での最新版のSolr 8.8ではいろいろと変わった部分があるので改めて採り上げてみます。
Nested Documents 機能について Solr 7.5 から 8.8 で変わった点
Solr 7.5 の頃と変わったのは以下の点です。
- 3層以上の階層を持てるようになった
- どの階層のドキュメントかを示すフィールドを明示的に定義しなくても良くなった。階層構造を保持する _nest_path_ フィールドを Solr が自動的に管理するようになったため
- 入れ子構造内の特定のドキュメントだけを部分的に更新できるようになった
スキーマ設定
Nested Documents を扱うためのスキーマ設定は以下の通りです。
_root_ フィールド (必須)
<field name="_root_" type="string" indexed="true" stored="false" docValues="false" />
インデックス作成時にSolrが自動的に作成するフィールドです。入れ子になったドキュメントはルートドキュメント(一番の先祖にあたるドキュメント)のidフィールドの値を _root_ フィールドの値として持ちます。
_nest_path_ フィールド (オプション)
<fieldType name="_nest_path_" class="solr.NestPathField" />
<field name="_nest_path_" type="_nest_path_" />
入れ子構造のルートドキュメント以外のドキュメントにSolrが自動的に付与するフィールドです。このフィールドは検索結果を取得するときに ChildDocTransformer で子ドキュメント以下の階層構造を扱うのに使われます。もしこのフィールドが無い場合はすべての子孫がフラットなリストとして扱われます。
_nest_path_ フィールドを使わない場合は、それぞれの階層を区別するための自前のフィールドを定義することが推奨されます。(手間が掛かるだけなので、素直に _nest_path_ フィールドを使った方が良さそうです)
_nest_parent_ フィールド (オプション)
<field name="_nest_parent_" type="string" indexed="true" stored="true" />
ルートドキュメント以外のドキュメントにSolrが自動的に付与するフィールドです。親ドキュメントのidフィールドの値を持ちます。
制限
- スキーマ内で各フィールド名の定義は1回だけ。親と子で同じフィールド名を別のフィールドタイプで定義するようなことはできない
- すべてのドキュメントタイプで共通するフィールドにだけ require を使うことができる
- 階層を問わず、すべてのドキュメントは id フィールドにユニークな値を持たなければならない
例: プレイリスト
以下のようなプレイリスト情報を Solr に与えます。
(_default の configset でコレクションを作っていればスキーマレスでインデックス作成できます)
[{
"id":"list_1",
"title_t":"list1",
"songs":[
{
"id":"l1!song1",
"title_t":"title1",
"artist_t":"artist1",
"trackNum_i":1
},
{
"id":"l1!song2",
"title_t":"title2",
"artist_t":"artist2",
"trackNum_i":2
}
]
},
{
"id":"list_2",
"title_t":"list2",
"songs":[
{
"id":"l2!song1",
"title_t":"title3",
"artist_t":"artist3",
"trackNum_i":1
},
{
"id":"l2!song2",
"title_t":"title1",
"artist_t":"artist1",
"trackNum_i":2
}
]
},
{
"id":"list_3",
"title_t":"list3",
"sublist":[
{
"id":"l3!sublist1",
"title_t":"sublist1",
"songs":[
{
"id":"l3!song1",
"title_t":"title4",
"artist_t":"artist4",
"trackNum_i":1
},
{
"id":"l3!song2",
"title_t":"title2",
"artist_t":"artist2",
"trackNum_i":2
}
]
},
{
"id":"l3!sublist2",
"title_t":"sublist2",
"songs":[
{
"id":"l3!song3",
"title_t":"title1",
"artist_t":"artist1",
"trackNum_i":1
},
{
"id":"l3!song4",
"title_t":"title3",
"artist_t":"artist3",
"trackNum_i":2
}
]
}
]
}]
プレイリスト名が list1 のプレイリストを曲も含めて検索
ヒットしたプレイリストの子ドキュメントを取得するために ChildDocTransformer を利用します。
$ curl 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="title_t:list1 -_nest_path_:*"}' -d 'fl=*,[child]'
{
"response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
{
"id":"list_1",
"title_t":"list1",
"_version_":1697935145902800896,
"songs":[
{
"id":"l1!song1",
"title_t":"title1",
"artist_t":"artist1",
"trackNum_i":1,
"_version_":1697935145902800896},
{
"id":"l1!song2",
"title_t":"title2",
"artist_t":"artist2",
"trackNum_i":2,
"_version_":1697935145902800896}]}]
}}
曲名が title1 の曲が含まれるすべてのプレイリストを検索
_nest_path_ フィールドでルート直下の songs の階層を指定しています。 {!parent} 内での -_nest_path_:* は「_nest_path_フィールドを持つもの以外」つまりルートドキュメントだけを対象とするという意味です。
$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:*"}(+_nest_path_:\/songs +title_t:title1)' -d 'fl=*,[child]'
{
"response":{"numFound":2,"start":0,"numFoundExact":true,"docs":[
{
"id":"list_1",
"title_t":"list1",
"_version_":1697935145902800896,
"songs":[
{
"id":"l1!song1",
"title_t":"title1",
"artist_t":"artist1",
"trackNum_i":1,
"_version_":1697935145902800896},
{
"id":"l1!song2",
"title_t":"title2",
"artist_t":"artist2",
"trackNum_i":2,
"_version_":1697935145902800896}]},
{
"id":"list_2",
"title_t":"list2",
"_version_":1697935145926918144,
"songs":[
{
"id":"l2!song1",
"title_t":"title3",
"artist_t":"artist3",
"trackNum_i":1,
"_version_":1697935145926918144},
{
"id":"l2!song2",
"title_t":"title1",
"artist_t":"artist1",
"trackNum_i":2,
"_version_":1697935145926918144}]}]
}}
サブリストに曲名が title1 の曲を含まれるプレイリストを検索
_nest_path_ フィールドで sublist 階層の下の songs の階層を指定しています。
$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:*"}(+_nest_path_:\/sublist\/songs +title_t:title1)' -d 'fl=*,[child]'
{
"response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
{
"id":"list_3",
"title_t":"list3",
"_version_":1697935153103372288,
"sublist":[
{
"id":"l3!sublist1",
"title_t":"sublist1",
"_version_":1697935153103372288,
"songs":[
{
"id":"l3!song1",
"title_t":"title4",
"artist_t":"artist4",
"trackNum_i":1,
"_version_":1697935153103372288},
{
"id":"l3!song2",
"title_t":"title2",
"artist_t":"artist2",
"trackNum_i":2,
"_version_":1697935153103372288}]},
{
"id":"l3!sublist2",
"title_t":"sublist2",
"_version_":1697935153103372288,
"songs":[
{
"id":"l3!song3",
"title_t":"title1",
"artist_t":"artist1",
"trackNum_i":1,
"_version_":1697935153103372288},
{
"id":"l3!song4",
"title_t":"title3",
"artist_t":"artist3",
"trackNum_i":2,
"_version_":1697935153103372288}]}]}]
}}