こちらに引き続き調べてみました。
DocValuesとStored&FieldCache
DocValuesはLuceneにおけるFieldの保存方式で Schemaに docValues="true"
を設定することで利用できます。
<field name="flong_cust" type="tlongs" indexed="true" stored="true" docValues="true" />
docValues
の他にも名前の通り保存のための設定 stored
があります。両方ともデータの保存方式なのですが何が違うのでしょうか。SlideShareに公開されていたドキュメントを引用してみます。
StoredされたFieldの動き
最初にstored
からstored="true"
が指定されたフィールドの値は .fdx
と.fdt
という拡張子のファイルに保存されます。読み込む際には .fdx
拡張子のインデックスファイルを読み込み、そこから取得したポインタ情報を持って .fdt
拡張子のフィールドデータファイルをシークして読みます。
この2回のディスクアクセスのコストがあるものの、個別のドキュメントIDに紐付いているstoredされた複数のフィールドのデータを一気にロードできる良さがあります。
上記の処理はこちらのクラスで行われているようです。
Storedの弱点とそれを補うFieldCacheの役割
上記のようにStoredされたFieldは、ドキュメントIDに紐付く全フィールドを取得するには良いのですが、例えばfacetやsort処理などのように、全ドキュメントの特定のフィールドだけを取得したい場合があります。そのような要件のために作られたのがFieldCacheと呼ばれるキャッシュです。
以下の図からデータの持ち方を見るとそれがわかりやすいかと思います。
このスライドが発表された当時では、以下のようにフィールドにアクセスできたことになります。全ドキュメントの特定のフィールドの値を取得できるイメージができると思います。(ちなみに最近のバージョンではAPI変わったので以下のようなアクセスはできません)
float[] weight = FieldCache.DEFAULT.getFloats(reader, "weight");
stored="true"
かつ docValues="false"
のフィールドを作ってSolrの以下の画面を見るとFieldCacheの中身が確認できます。
http://[solr_host]:[solr_port]/solr/#/[core_name]/plugins?type=cache&entry=fieldCache
このように FieldCache
は stored
の弱点の補完をしています。
特定のフィールドが最初にリクエストされたときなどにキャッシュが構築されるのですが、その際のキャッシュのロードに時間がかかることや、Javaのヒープメモリ利用量が大きくなってしまうなどの課題がありました。
そこで出てきたのがDocValuesです。
DocValuesについて
まずはおさらいから。上記の図でもわかる通り、storedされたフィールドはドキュメントIDに紐付いた全stored fieldのデータが並んだ形をしています。jsonで表すとこのような感じです。
{ 'doc1': {'fieldA':1, 'fieldB':2, 'fieldC':3}, 'doc2': {'fieldA':2, 'fieldB':3, 'fieldC':4}, 'doc3': {'fieldA':4, 'fieldB':3, 'fieldC':2} }
このような行指向に対して、DocValuesは列指向でデータを格納します。jsonで表すと以下のような感じです。上記のFieldCacheと同様に特定の特定のフィールドの値を取得するのに適しています。
{ 'fieldA': {'doc1':1, 'doc2':2, 'doc3':4}, 'fieldB': {'doc1':2, 'doc2':3, 'doc3':3}, 'fieldC': {'doc1':3, 'doc2':4, 'doc3':2} }
DocValuesは、この特定のフィールドの値を取得しやすいデータ構造をインデクシング時に構築してしまいます。列指向データベースなどと同様データは圧縮されて、OSにインデックスファイルとしてに出力されます(拡張子 .dvd
と .dvm
)。
インデクシング時にデータを構築することで、FieldCacheのようにロード処理によるサービス影響がなくて済みます。
FieldCacheがJavaのヒープを利用するのに対して、DocValuesはOSキャッシュに乗せて動かします。
それぞれFieldTypeとLuceneのクラスの対応です。
FieldType | multiValued | Luceneのクラス |
---|---|---|
StrField/UUIDField/Boolean | false | SORTED |
StrField/Boolean | true | SORTED_SET |
Trie*(TrieInt)/PointFields | false | NUMERIC |
Trie*(TrieInt) | true | SORTED_SET |
Trie*(TrieInt)/PointFields/Boolean | true | SORTED_NUMERIC |
Solr側はこのあたり、
Lucene側はこのあたりを参考。
lucene-solr/DocValuesType.java at releases/lucene-solr/6.6.0 · apache/lucene-solr · GitHub
Lucene側のもっと細かい動きを知りたければこの辺りかと思います。
また、ここまでFieldCacheとDocValuesを続けて説明してきましたが、現在では以下のIssueによりFieldCacheのAPIはDocValuesのAPIに統合されています。
Release 5.0.0 [2015-02-20] [LUCENE-5666] Add UninvertingReader - ASF JIRA
Change uninverted access (sorting, faceting, grouping, etc) to use the DocValues API instead of FieldCache. For FieldCache functionality, use UninvertingReader in lucene/misc (or implement your own FilterReader). UninvertingReader is more efficient: supports multi-valued numeric fields, detects when a multi-valued field is single-valued, reuses caches of compatible types (e.g. SORTED also supports BINARY and SORTED_SET access without insanity). “Insanity” is no longer possible unless you explicitly want it. Rename FieldCache and DocTermOrds classes in the search package to DocValues*. Move SortedSetSortField to core and add SortedSetFieldSource to queries/, which takes the same selectors. Add helper methods to DocValues.java that are better suited for search code (never return null, etc).
DocValues の注意点
Solrバージョン毎の機能制限を確認する
Luceneで開発されたDocValuesにSolrが6系統から本格的に対応しだしているので、直近のSolrのバージョンアップによりDocValuesに関わる部分にも大きなアップデートが頻繁にありました。
例えばSolr 6.2ではBoolean型にも対応しましたし、
Solr 6.5ではPointTypeにも対応しています。
[SOLR-9987] Implement support for multi-valued DocValues in PointFields - ASF JIRA
MultiValueの順序保証
マニュアルに以下の記載があるとおり
In the past, Solr guaranteed that retrieval of multi-valued fields would preserve the order of values. Because values may now be retrieved from column-stored fields (docValues=“true”), in conjunction with the fact that DocValues do not currently preserve order, means that users should set useDocValues AsStored=“false” to prevent future optimizations from using the column-stored values over the
実際に動かしてみます。
このようなSchemaを設定して、
<field name="storedlong" type="tlongs" indexed="true" stored="true" multiValued="true" docValues="false" required="true"/> <field name="dvlong" type="tlongs" indexed="true" stored="false" multiValued="true" docValues="true" required="true"/> <fieldType name="tlongs" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0"/>
以下のドキュメントを登録します。
{"id":"1", "storedlong":["5","3","100","10","1"], "dvlong":["5","3","100","10","1"]}
レスポンスを見てみます。 dvlong
の方はMultiValueの数値が昇順にソートされてしまっています。
{ "response":{"numFound":1,"start":0,"docs":[ { "storedlong":[5, 3, 100, 10, 1], "dvlong":[1, 3, 5, 10, 100]}] }}
DocValuesとStoredの設定によるクエリ挙動の違い
以下のSchemaで挙動確認してみます。
<field name="storedlong" type="tlongs" indexed="true" stored="true" multiValued="true" docValues="false"/> <field name="dvlong" type="tlongs" indexed="true" stored="false" multiValued="true" docValues="true" /> <field name="dvstoredlong" type="tlongs" indexed="true" stored="false" multiValued="true" docValues="true" useDocValuesAsStored="true"/> <fieldType name="tlongs" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0"/>
普通にクエリで q=*:*
と呼び出した場合、stored="false" docValues="true"
のフィールドは返却されません。
stored="false" docValues="true"
に useDocValuesAsStored="true"
を追加したフィールドは返却されます。
{"response":{"numFound":1,"start":0,"docs":[ { "id":"1", "storedlong":100, "_version_":1574440653246431232, "dvstoredlong":100}] }}
stored="false" docValues="true"
の dvlong
に関してもデータが保存されていないわけではなく、 fl
パラメータで指定することでデータが返却されます。
{ "responseHeader":{ "status":0, "QTime":1, "params":{ "q":"*:*", "indent":"on", "fl":"dvlong", "wt":"json", "_":"1501475815516"}}, "response":{"numFound":1,"start":0,"docs":[ { "dvlong":100}] }}
一見、stored
と同じ動作に見える useDocValuesAsStored="true"
にも注意が必要で、あくまでDocValues形式なのでstoredされているフィールドと違いMultiValueの順序が保証されていません。
例えば以下のデータを登録してみて、
{"id":"1", "storedlong":["5","3","100","10","1"], "dvlong":["5","3","100","10","1"], "dvstoredlong":["5","3","100","10","1"]}
結果は以下の通りになります。 useDocValuesAsStored
指定されていてもDocValuesとしてしか保存されていないFieldはMultiValueの順序保存されません。ちなみに当然ながら docValues="true"
に合わせて stored="true"
が指定されていれば順序はstored側で保持されますので結果もstoredに合わせて順序保持された状態のものになります。
{ "response":{"numFound":1,"start":0,"docs":[ { "id":"1", "storedlong":[5, 3, 100, 10, 1], "_version_":1574440140270469120, "dvstoredlong":[1, 3, 5, 10, 100]}] }}
一度ロードされたFieldCacheと比較して早いわけではない
DocValuesはインデクシング時に構築されることによる、リクエスト時の初回ロードが不要であることや、Javaのヒープを圧迫しないこと、圧縮も効いていてディスクサイズを節約できることなどメリットはあります。
しかし、クエリのパフォーマンスという観点で見ると、一度キャッシュされてしまったFieldCacheと比較して特別早いわけではないです。むしろ遅いくらい。新しく導入する際にはパフォーマンステストをした方が良いかと思います。そもそもJavaのヒープに全部乗らないという時が多いのでDocValuesがあったりもするのですが。
「一度キャッシュされてしまったFieldCaccheと比較して」と書いた通り、FieldCacheをJavaHeapにキャッシュには時間がかかります。キャッシュが落ちてしまうインデックス更新のタイミングなどで、レスポンス悪化を許容したり、サービス影響無いようにキャッシュを制御できることが条件になりますが。
ここに記載のあるようにインデクシング時などSearcherが新規で立ち上がる時に特定のクエリ投げてあげたりするのも手です。
https://wiki.apache.org/solr/SolrConfigXml#newSearcher
参考
Apache Lucene - Index File Formats
DocValues | Apache Solr Reference Guide 6.6
[LUCENE-5666] Add UninvertingReader - ASF JIRA
[LUCENE-6840] Put ord indexes of doc values on disk - ASF JIRA