Lucene/Solr DocValuesについて調べたことをまとめてみた

こちらに引き続き調べてみました。

yomon.hatenablog.com

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された複数のフィールドのデータを一気にロードできる良さがあります。

上記の処理はこちらのクラスで行われているようです。

https://github.com/apache/lucene-solr/blob/releases/lucene-solr/6.6.0/lucene/core/src/java/org/apache/lucene/codecs/compressing/CompressingStoredFieldsReader.java

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

このように FieldCachestored の弱点の補完をしています。

特定のフィールドが最初にリクエストされたときなどにキャッシュが構築されるのですが、その際のキャッシュのロードに時間がかかることや、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-solr/solr/core/src/java/org/apache/solr/schema at releases/lucene-solr/6.6.0 · apache/lucene-solr · GitHub

Lucene側はこのあたりを参考。

lucene-solr/DocValuesType.java at releases/lucene-solr/6.6.0 · apache/lucene-solr · GitHub

Lucene側のもっと細かい動きを知りたければこの辺りかと思います。

lucene-solr/lucene/core/src/java/org/apache/lucene/codecs/lucene54 at releases/lucene-solr/6.6.0 · apache/lucene-solr · GitHub

また、ここまで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-9187] Support dates and booleans in /export handler, support boolean DocValues fields - ASF JIRA

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

参考

lucidworks.com

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