よしだのブログ

サブタイトルはありません。

Solr で使う malloc を差し替えてみたら幸せになれるかもしれない話

こんにちは!この記事は Solr Advent Calendar 2017 の1日目の記事です! https://qiita.com/advent-calendar/2017/solr

さて、最近のハードウェア、特に RAM の容量の増加と QPS の増加などに伴い、それにあった malloc が登場してきています。特に有名なのは google が作った tc malloc だったりするのですが、この malloc に差し替えた状態で MySQL を動かすとパフォーマンスが向上するという調査が合ったりします。

www.percona.com

そこで、Solr も malloc 差し替えると速くなるんじゃね?!と思い立ってやってみた、という記録です。

前提知識 : malloc について

Cで使われるメモリアロケーターのライブラリ関数。指定された分量だけメモリを確保して、確保できたメモリのポインタを返すというもの。

http://www9.plala.or.jp/sgwr-t/lib/malloc.html

色々な malloc

普段使っているのは、ほとんどは glibc malloc だと思いますが、大容量の RAM を踏まえた malloc のアルゴリズム&実装が生み出されています。

glibc malloc

RHEL などで デフォルト、デファクト・スタンダードと呼んでよいと思います。 基本的な特長は、ロックがぶつからない限りアリーナを共有するというもの。glibc malloc の仕組みを理解するには以下を見ること (長いですがめちゃくちゃおすすめです)

youtu.be

資料 https://www.slideshare.net/kosaki55tea/glibc-malloc

tcmalloc

google 製 最初っから1スレッド1アリーナでアリーナを共有しない、したがってロックがぶつかることがないので、その分速い。

TCMalloc : Thread-Caching Malloc

jemalloc

FreeBSDでは標準に採用されている malloc arena、もしくはarenaの中のサイズクラスごとにLock を持つことでスレッド間の排他を削減する

http://www.canonware.com/jemalloc/

vespa_malloc

Yahoo Inc. より公開された OSS の検索エンジン vespa で使用されている malloc vespa に最適化されている?

vespa.ai

glibc malloc の課題

スレッドが複数立つ環境において glibc malloc の性能はあまり良くないと言われている。多数のスレッドが稼働する環境では、CPU数を増やしていってもスループットが上がってこないと言われている。頻繁な malloc / free の繰り返しに特に弱く、Java などを始めとしたオブジェクト指向のプログラミング言語では、malloc / free が多くなりがち。

以下は malloc / free を10万回繰り返して、そのタイムを比較した検証結果。glibc では CPU を増やすとむしろ遅くなっている。

https://www.soum.co.jp/misc/individual/multi-thread/

検証内容

各 malloc に差し替え、それぞれパフォーマンスを計測比較しました。

  • 検索のみ実行 (更新は行わず)、予めキャッシュはウォームアップした状態で実施
  • Solr 6.4.2 を使用
  • 1 shard / 2 replica 構成、SolrCloud
  • 検索対象は、170万件、インデックスサイズは約 1.3 GB
  • 使用した malloc は以下の通り
    • glibc malloc 2.12-1.209
    • tcmalloc 1.7-5
    • jemalloc 3.4.0.3
    • vespa_malloc 6.150.49
  • 使用したサーバーは仮想マシン
    • 16 vCPU / 24GB RAM / 120GB Disk (SSD)
  • Solr のキャッシュサイズはデフォルトのまま変更なし
  • 3000 QPS の負荷を1時間かけ、トータルの QPS を計測

計測結果

各 malloc ごとのスループットは以下の通りでした。jemalloc が最も数値が良いものの、tc malloc / vespa_malloc も誤差の範疇でとても良い数字が出ました。

  • glibc malloc : 2848.91 Q/s
  • tc malloc : 3152.74 Q/s (+10%)
  • je malloc : 3174.92 Q/s (+11%)
  • vespa malloc : 3117.68 Q/s (+9%)

というわけで、malloc を置き換えをするととても速くなりそうなので、さらなる検証をかさねているところです。 今回できるだけ参考になるように、一般化した状態で計測を試みてみましたが、仮想マシンと物理マシンだったり、利用しているクラウドサービスの違いだったり、Solr や OS のバージョンの違いだったり、色々と差異はどうしてもあると思うので、是非一度、計測してみることをおすすめします。

【Lucene/Solr 7.0】index-time boost の無効化を理解するための前提の調査

Lucene/Solr 7 が先日リリースされました! Lucene/Solr 7 の新機能も気になるところですが(別エントリでまとめようかと)、7 では index-time boost の機能が無効になるとの情報をキャッチしました。

詳細な内容は上記の関口さんのブログに詳しいのですが、boost のスコアの扱いについてよくわかっていない部分があり、無効になった場合の影響度合いがピンと来ていなかったので、ちょっと調べなおして見ました。このエントリを読んでいただければ、boost のスコアに与える影響について理解を深められ、index-time boost の無効化があなたが管理している Solr を使ったシステムへどういう影響を与えるか、すこし見えてくるかなと思います。

前提知識

疑問その1、index-time boost と query boost はスコアに与える影響は同じなのか違うのか

index-time boost が無効になるなら、代わりに query boost を使えばいいじゃない!と思ったのですが、そもそも代わりになるのか?というところについて調べたいなと思いました。 index-time で boost 値を 2 と指定した場合と、query で指定したときとではスコアが変わるかどうか? という点について調べてみました。

疑問その2、omitNorms=true にした場合の挙動の違い (BM25 / TF-IDF)

omitNorms=true にすると何が起きるかというと、ClassicSimilarity では TF-IDF にもとづいてスコアが計算されるのですが、Lucene の ClassicSimilarity はオリジナルの式に手が加えられており、フィールドに含まれる文書の長さとboot値を考慮するように実装されており、その「フィールドの文章の長さと boost 値の考慮」が無効になります。

一方、BM25Similarity ではオリジナルの式の時点でフィールド長を考慮することが含まれています。Lucene ではこれに boost 値を考慮するように実装されているはず、という点と、omitNorms=true にすると ClassicSimilarity の用に式の該当箇所が無効になるのか?という疑問になります。

結論

結論から先に書くと以下の通りでした。

  • 疑問その1について、index-time boost を使用している場合は query time boost の導入や、doc value + function query による boost などの代替手段の検討が必要だが、query time boost と index-time boost では式の影響する箇所が異なるり、boost 値やドキュメントとクエリが全く同じでも違うスコアになることから十分なテストが必要。また、そもそも omitNorms=true なフィールドについては index-time boost はそもそも設定ができないので、index-time boost の無効化による影響はないので、こちらは心配しなくても大丈夫。

  • 疑問その2について、BM25Similarity のフィールド長に対する考慮も ClassiSimilarity と同様に、omitNorms=true の場合、スコア計算から除外されるようになっている。同じような挙動になるが、スコアやスコアを計算する式に対する影響は全く異なるので注意が必要。

したがって、Solr 7 を導入する場合は、omitNorms=true が指定されていないフィールドに index-time boost を使用している場合に限られるが、全く同じスコアになる代替手段は提供されていないので、十分なテストが必要、という理解をしました。

また、以下のようなことがわかりました。

  • query time boost と index-time boost の影響する箇所は全く違う。index-time boost はスコアを計算する式の変数の値が変化するのに対して、query time boost は、指定された boost 値をプレーンなスコアに単純に乗算するので、index-time boost よりも影響が強い。

    • BM25 の場合、index-time boost の値を大きくすると計算式の変数 fieldLength の値が小さくなりスコアが上昇する、ClassicSimilarity の場合は計算式の変数 fieldNorm の値が大きくなりスコアが上昇する (下記、調査その1:index-time boost をセットしたときのスコアの変化を参照)
    • 2種類の index-time boost の doc boost / field boost のいずれも上記と同様に fieldLength / fieldNorm の値に影響する、また別の変数の値には影響しない
    • BM25 の b の値は、いわゆる boost の値ではない (schema.xml で別途指定できる)。クエリやドキュメントでは調整できない。
  • omitNorms を true にした場合のスコアの変化は、使用する Similarity によって全く違うので要注意。

    • Classicsimilarity の場合、omitNorms=true に設定すると fieldNorm が固定で 1.0 になる、BM25 の場合、omitNorms=true に設定するとフィールド長の影響を計算する変数 b / avgFieldLength / fieldLength が使用されないように式が変化する。 (下記、調査その2:omitNorms=true にした場合のスコアの変化を参照)

(ところで、BM25 の explain は計算式まで表示されるのでとっても親切ですね!)

調査その1: index-time boost をセットしたときのスコアの変化

index-time boost の値を設定した場合のスコアの変化と変化する箇所を確認する いずれも Solr 6.4.2

BM25Similarity

index-time boost の値を大きくすると fieldLength の値が小さくなり、結果スコアが大きくなる

index-time boost なし

9.910029 = weight(Description_sand:ローソン in 0) [SchemaSimilarity], result of:
  9.910029 = score(doc=0,freq=1.0 = termFreq=1.0
), product of:
    7.991893 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
      4.0 = docFreq
      13305.0 = docCount
    1.2400103 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
      1.0 = termFreq=1.0
      1.2 = parameter k1
      0.75 = parameter b
      30.368282 = avgFieldLength
      16.0 = fieldLength

field boost のみ

12.394508 = weight(Description_sand:ローソン in 0) [SchemaSimilarity], result of:
  12.394508 = score(doc=0,freq=1.0 = termFreq=1.0
), product of:
    7.991893 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
      4.0 = docFreq
      13305.0 = docCount
    1.5508852 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
      1.0 = termFreq=1.0
      1.2 = parameter k1
      0.75 = parameter b
      30.368282 = avgFieldLength
      4.0 = fieldLength

doc + field boost

13.330253 = weight(Description_sand:ローソン in 0) [SchemaSimilarity], result of:
  13.330253 = score(doc=0,freq=1.0 = termFreq=1.0
), product of:
    7.991893 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
      4.0 = docFreq
      13305.0 = docCount
    1.6679718 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
      1.0 = termFreq=1.0
      1.2 = parameter k1
      0.75 = parameter b
      30.368282 = avgFieldLength
      0.64 = fieldLength

ClassicSimilarity

fieldNorms の値が変化し、スコアが増える

index-time boost なし

19.742617 = weight(Description_sand:ローソン in 0) [SchemaSimilarity], result of:
  19.742617 = score(doc=0,freq=1.0), product of:
    8.886533 = queryWeight, product of:
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      1.0 = queryNorm
    2.2216332 = fieldWeight in 0, product of:
      1.0 = tf(freq=1.0), with freq of:
        1.0 = termFreq=1.0
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      0.25 = fieldNorm(doc=0)

field boost のみ (2 * 0.25 = 0.5)

39.485233 = weight(Description_sand:ローソン in 0) [SchemaSimilarity], result of:
  39.485233 = score(doc=0,freq=1.0), product of:
    8.886533 = queryWeight, product of:
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      1.0 = queryNorm
    4.4432664 = fieldWeight in 0, product of:
      1.0 = tf(freq=1.0), with freq of:
        1.0 = termFreq=1.0
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      0.5 = fieldNorm(doc=0)

field + doc boost (22.50.25=1.25)

98.71308 = weight(Description_sand:ローソン in 0) [SchemaSimilarity], result of:
  98.71308 = score(doc=0,freq=1.0), product of:
    8.886533 = queryWeight, product of:
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      1.0 = queryNorm
    11.108166 = fieldWeight in 0, product of:
      1.0 = tf(freq=1.0), with freq of:
        1.0 = termFreq=1.0
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      1.25 = fieldNorm(doc=0)

調査その2: omitNorms=true にした場合のスコアの変化

BM25 では omitNorms=true なフィールドと false なフィールドに対して同じ q を送信し、スコアの explain を確認、計算内容の違いを調べる いずれも Solr 6.4.2

BM25Similarity

omitNorms=true にすると b / avgFieldLength / fieldLength が計算式に含まれない

1 : omitNorms=false なフィールド

5.110843 = weight(Description_sand:ローソン in 29) [SchemaSimilarity], result of:
  5.110843 = score(doc=29,freq=1.0 = termFreq=1.0
), product of:
    0.5 = boost
    8.243133 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
      3.0 = docFreq
      13304.0 = docCount
    1.2400244 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
      1.0 = termFreq=1.0
      1.2 = parameter k1
      0.75 = parameter b
      30.369589 = avgFieldLength
      16.0 = fieldLength

2 : omitNorms=true なフィールド

5.072106 = weight(Title_search_sand:ローソン in 965) [SchemaSimilarity], result of:
  5.072106 = score(doc=965,freq=2.0 = termFreq=2.0
), product of:
    0.5 = boost
    7.377609 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
      12.0 = docFreq
      19996.0 = docCount
    1.375 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1) from:
      2.0 = termFreq=2.0
      1.2 = parameter k1
      0.0 = parameter b (norms omitted for field)

ClassicSimilarity

omitNorms=true にすると fieldNorm の値が 1.0 固定になる

1 : omitNorms=false なフィールド

19.742617 = weight(Description_sand:ローソン in 0) [SchemaSimilarity], result of:
  19.742617 = score(doc=0,freq=1.0), product of:
    8.886533 = queryWeight, product of:
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      1.0 = queryNorm
    2.2216332 = fieldWeight in 0, product of:
      1.0 = tf(freq=1.0), with freq of:
        1.0 = termFreq=1.0
      8.886533 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        4.0 = docFreq
        13305.0 = docCount
      0.25 = fieldNorm(doc=0)

2 : omitNorms=true なフィールド

96.589584 = weight(Title_search_sand:ローソン in 965) [SchemaSimilarity], result of:
  96.589584 = score(doc=965,freq=2.0), product of:
    8.26433 = queryWeight, product of:
      8.26433 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        13.0 = docFreq
        19997.0 = docCount
      1.0 = queryNorm
    11.687528 = fieldWeight in 965, product of:
      1.4142135 = tf(freq=2.0), with freq of:
        2.0 = termFreq=2.0
      8.26433 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
        13.0 = docFreq
        19997.0 = docCount
      1.0 = fieldNorm(doc=965)

参考リンク

LUCENE-6819: Good bye index-time boost http://lucene.jugem.jp/?eid=485

BM25 The Next Generation of Lucene Relevance http://opensourceconnections.com/blog/2015/10/16/bm25-the-next-generation-of-lucene-relevation/

(fieldNorm は浮動小数点で8ビット=1バイトに保存しており、lengthNorm * boot値を保存しないといけない。boost の値は index-time boost で好きな値が設定できるので、豪快に丸め誤差が発生する・・)

【Lucene / Solr】G1GC か CMS か?

こんばんは!宿題が遅れてすいませんw 今日は、ちょっとポエムみたいになるので余り役に立たないかもしれませんが、G1GC とOSSコミュニティのお話です。

お約束の、この記事は、Solr Advent Calendar 19日目の記事です。

qiita.com

きっかけ

ツイッター某所で G1GC がインデックスを壊すことがあるというようなつぶやきを見かけたのがそもそもの始まりでした。 以下が張られていたリンクで、ElasticSearch 2.X のユーザーマニュアルの一部でして、内容は変更してはいけない設定について記載されたものです。

www.elastic.co

その中にガベージコレクションの設定についてのセクションが有り、以下のような記載があります。

  • デフォルト GC を変更するな (CMS、G1GC はインデックスを壊すことがある)
  • G1GC は素晴らしいがいまだに定期的にバグが見つかっている

結構強い表現で、なかなか衝撃を受けたのですが、これって Lucene をコアに使うSolr もかんけいあるんじゃね?と思い調査をはじめました。

先に結論、G1GC 自己責任でどうぞ

結論から言えば、コミュニティとしての統一見解はないそうです。G1GC をもう使ってもいいかもしれないけど自己責任でね、ということです。色々調べてみた内容を書くと以下の通りです。

Lucene Wiki

まず、Lucene wiki には Java / JVM のバグに関してページがあり、以下のように記載があります。(2015/1/9 #31 RovertMuir) どのような状況下でも Lucene を G1GC で走らせるな、と結構強い口調で書かれています。ちょうど、最終更新時にこの一文が書き足されたようです。

Do not, under any circumstances, run Lucene with the G1 garbage collector. Lucene's test suite fails with the G1 garbage collector on a regular basis, including bugs that cause index corruption. https://wiki.apache.org/lucene-java/JavaBugs

開発者向けメーリングリスト

一方でメーリングリストでは、G1GC はだいぶこなれてきたので使ってもいいんじゃない?という投稿がされます。(2015/1/2 Shawn Heisey-2) 投稿者自身はG1GCを使っていて一度も問題に合ったことは無いと書いています。 一方で Mark Miller などは Aggerssive Option と呼んでおり、パフォーマンスとリスクを図りにかけ選択すべきと書いています。

I've been working with Oracle employees to find better GC tuning options.  The results are good enough to share with the community

Depending on your needs and risk tolerance, you might make a different choice. http://lucene.472066.n3.nabble.com/Garbage-Collection-tuning-G1-is-now-a-good-option-td4176927.html

某ニュースサイトの記事

また、全く別のJavaのニュースサイトの記事 (2015/1/26 jaxenter) では、G1GC と Lucene のこの辺の戦いの経緯が紹介されています。 (よく見ると、このエントリーを書いたのは lucene と solr の commiter である Uwe Schindler でした)

その中で、Oracle Java 8 update 40 のアナウンスで G1GC は production ready だと発表が合ったことや、最近の Lucene のビルドではこれらのエラーが見られなくなったと記載しています。 しかし一方で、かつて起こっていたエラーを理解している人が誰もおらず、ただエラーが出なくなっているだけであるとも記載しています。

When observing the Lucene builds during recent months, the Lucene team noticed that the errors initially seen no longer occurred. This is also consistent with the statement by Oracle that G1GC is “ready for production” in Java 8 Update 40.

However, one may still feeling bad when putting it into production, because some of the errors were never understood; they simply no longer occur – there is nothing more one knows about. https://jaxenter.com/java-9s-new-garbage-collector-whats-changing-whats-staying-118313.html

とどめに Lucene Solr Revolution 2016: Stump the Chump 2016 より

先日の Solr 勉強会に参加した際に Lucene Solr Revolution の Stump the Chump で喋っていたよー、という情報をキャッチし早速確認してみました。動画はご覧になれますので是非どうぞ。トップコミッター陣が経緯を含めてきちんと説明しております。

www.youtube.com

以下、喋っている内容のメモ書きです。

  • 31分ごろから39分ぐらいまで
  • Oracle が JDK 9 から G1GC をデフォルトにしようとしているがどう思いますか? Solr のデフォルトを G1GC にする予定は有りますか? solr を CMS / G1GC で使うメリットデメリットは有りますか?
    • bin/solr にはデフォルトの jdk も gc もコーディングしていないので、動かしている環境の設定が使われる
      • 正しくは、gc は CMS がデフォルトとしてコーディングされている (後に指摘が入る)
    • G1GC がダメかどうかも、pros / cons も分からないがテストはしっかりやるべき
    • G1GC でインデックスを壊すことが単体テストで確認されていた(今は直されている)
    • CMS でも同様に壊れるケースがあった
    • Mark Miller : 個人的な意見では 60GB 以上のような大きなヒープサイズでは g1gc は良いオプション、30GB 以下のヒープであれば CMS はよくテストされていて安定しているのでこれまでデフォルトにしてきたしおすすめしてきた。また、docValue の登場でそれほど大きなヒープサイズは必要としなくなったので、誰かが変えようとしない限り CMS のままだと思う。一方で、たくさん g1gc を使っているが特に問題は起きていないお客さんもたくさんいる。
    • java 8 でのパフォーマンス計測では g1 が良いという結果もでている
    • lucene の website (多分 lucene wiki のことだと思う) にあるコメントは g1 を使うなと書いてあるが、それはひどくクラッシュすることがあったので書かれたが、一人のコミッターが書いたのであってコミュニティの総意を示しているわけではない
    • 観客のコメント)5000台の solr を CMS から g1gc にスイッチしたが、リカバリーの原因になるような 15秒を超える gc の回数は確実に減った

Lucene Solr Revolution 2016 の初日なので 2016/10/13 のコミッターの発言です。この Stump the Chump というセッション自体が、お酒を飲みながらフランクにコミッターが質問にこたえるよ!というないようなので、適当なことを言っているのかと思いきや、結構ちゃんとしたことをいっているw

Java 8 Update 40 以降、G1GC 絡みのバグは減っているのか?

一応見てみたけど、よくわかりませんね。。

  • Oracle Java のほうは、database の検索の絞込が使えずわからない。
  • 参考で Open JDK については、多くないが Java 8 にも影響する G1 の問題がまだたくさんあることがわかる。

https://bugs.openjdk.java.net/browse/JDK-7178365?jql=project%20%3D%20JDK%20AND%20issuetype%20%3D%20Bug%20AND%20status%20in%20(Reopened%2C%20Open%2C%20%22In%20Progress%22%2C%20New)%20AND%20text%20~%20G1

調査のまとめ

まとめると、以下のような状況であることがわかりました。

  • ElasticSearch では、G1GC は使わずに CMS を使うべきとしている
  • Lucene/Solr については、統一的な見解を見つけることはできなかった。コミッターにより温度感はまちまちの模様。G1GC 使ってもいいかもしれないけど、テストしてからがいいんじゃね?とのこと。
  • OpenJDK には G1 に関するバグがまだたくさんあるが、Lucene にどのぐらい影響があるかはわからない

個人的には G1GC が原因でインデックスがロストした、という事象を聞いたことはないので、まあ、使ってもいいのかなと思ったりするのですが、使い方や設定、バージョンや Oracle なのか Open なのかによりけりな部分もかなり大きいのでテストしましょ、というのがやはり正解だと思います。Oracle Java 9 からはデフォルトで使用する、という熱の入れようなようなので、Java 9 で安定することに期待しつつ。

【Solr】クエリのオペレータが無視される、仕様?!

こんにちは!今日は Solr の小ネタを書こうかと思います。この記事は Solr Advent Calendar 2016 の16日目の記事です!

qiita.com

qf に存在しないフィールドを含めると、q に指定したオペレータが検索キーワードとして扱われる。

今日ご紹介するのは、罠というかバグのような Solr の仕様です。対象の Solr のバージョンは 5.x 〜 trunk まで全てです。

Solr で edismax を使う際に、よく検索対象としたいフィールドを指定する際に qf パラメータを使うと思います。この、qf でキーワード検索したいフィールド名を羅列しておくと、q パラメータでフィールド名を指定せず、キーワードだけ指定することで qf で指定したフィールドを横断的に検索してくれます。

この qf に shema 定義に存在しないフィールド名を指定するとどうなるかというと、なんと q で指定した AND や OR などのオペレータが単なる文字列として検索されます。。

例)hogefoobar というフィールドは存在しないケース

q : 日本 OR タバコ defType : edismax qf : title hogefoobar

/select?q=日本 OR タバコ&defType=edismax&qf=title hogefoobar

上記のようなパラメータで検索すると、title フィールドに「日本」「OR」「タバコ」のいずれかのキーワードが含まれるドキュメントがヒットします。。

原因を調べてみたところ、以下の箇所で クエリのパース時に Exception を受け取ると、q の中身をエスケープして再検索するようになっています。qf に適当なフィールド名を指定すると、FieldNotFoundException が投げられ、この箇所でキャッチしエスケープして再検索しているため、OR は文字列として扱われます。。

https://github.com/apache/lucene-solr/blob/master/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java#L310-L313

まとめ

というわけで、雑に qf に適当なフィールド名をつけて投げるとひどい目に合うよということで、きちんと qf には存在しているフィールドを指定しているか確認しましょう。 特に managed schema (スキーマレスモード) を使用していると、フィールドの管理が雑になりがちなので注意が必要だと思います。

一応

パッチを送っていたりしますが・・マージはされないかもしれません。

[SOLR-9677] edismax treat operator as a keyword when a query parameter 'qf' contains inexist field. - ASF JIRA

第19回Lucene/Solr勉強会 #SolrJP

こんにちは!久しぶりの Lucene Solr 勉強会です。 メモを公開しますー。

NLP4Lを使ったランキング学習


株式会社シーマーク 山本 高志 様

www.slideshare.net

講演内容メモ

  • Apache Lucene のための自然言語処理ツール (OSS) github.com

  • 何ができるか?

    • 固有表現抽出
    • 文書分類
    • キーフレーズ抽出
    • ランキング学習 (今日のテーマ)
    • など・・
  • 標準提供のランキング学習モデル
    • PRank
    • RankingSVM
    • など・・
  • ランキング学習のフロー
    • クエリログ・アノテーションを教師データとして使用する (作成を支援するツールも含まれる)
    • Feature の抽出、トレーニング、モデル配置までできる
    • モデル評価の機能は未実装だが今後やっていきたい
  • Featrure は Lucecene で取得できる値を利用する
    • TF/IDF や BM25 など
  • リランキングは Solr の標準の機能を使用している
    • ランキングモジュール自体は NLP4L に含まれる PRank や RankingSVM をクエリで指定する
  • Bloomberg 版 LTR との比較

一言・感想

  • UI がついているので教師データの作成は非常にやりやすそうです。大規模なケースで複数人で手分けする場合にはどのようにできるかが気になった
  • Bloomberg 版とのもう少し詳細な比較が見たいです。

Lucene/Solr Revolution 2016参加レポート

楽天株式会社 中田 晋平 様


www.slideshare.net

講演内容メモ

  • 3点ピックアップして紹介

  • Working with Deeply Nested Documents in Apache Solr

    • 昔は Solr はいまいちだったが、5系ならイケる
    • 普通の入れ子の場合(2段階まで)
      • Lucene は flat な index しか持てないので、連続した docid に親子のデータを配置する、親子の区別用にもう一つのフィールドを利用する (boolean の isParent みたいな)
      • Block Join Query
    • Deeply Nested Document の場合
      • 基本的に Nested と同じ、どの階層でも独立した1つのドキュメント
      • path という考え方を導入、親子の構造を string のフィールドに含める、簡単に処理できるように PreProcessor を用意した
      • 親子孫含めて横断的に検索するには、v5.3 以降の ChildDocTransformerFactory を利用するとできる
  • Rebalancing API for SolrCloud

    • SolrCloud の運用を楽にする API を作成した
    • Rebalancing API を作成し、Solr に contribute (6系に含まれる?)
    • Re-sharding
      • 再インデキシング不要でシャードを増やす
      • 別の shard を用意して一旦マージした後に再度分割する
    • マイグレーション
    • 冗長化 (レプリカを増やす)
    • Reindexing なしなので速い、設定ファイルも一緒に配布する
  • The Evolution of Lucene Solr Numerics from Strings to Pints

    • 数値の文字列表現について
      • 最初は String で全て持っていた
      • 2009 年、trie numerics に置き換え
        • trie 木の一部にデータが集中するデータは非効率になっていた
      • 2016 年、dimentional point に置き換えを予定 (lucene にはすでに入っている)
        • k dimension tree を使用する
        • それぞれの次元に対して繰り返し分割する、分割した結果を元に tree を作成する、分割は中央値で分割する
        • ノードが一定数まで分割して終了する
        • データの密度に応じてバランスする

一言・感想

  • G1GC が OK というのがびっくりですね!私も参加していたのですが、知りませんでした。 (TODO: あとでしらべる)

Solrを活用した施設情報検索システムの取組み

株式会社NTTドコモ 榎園 健 様



講演内容メモ

  • 施設情報検索に特化した検索エンジン
    • 施設名称・所在地を考慮して検索 (横浜のコストコとか)
  • 課題1:地名解釈
    • 「横浜 ドコモ」で検索しても、ドコモショップ 戸塚店はヒットしない (戸塚店は、横浜市戸塚区にある)
    • edismax は、スコアが最大のフィールドのみを採用するので、地名でヒットしてもスコアは採用されない場合がある
    • プラグインを開発
      • クエリ内に地名が存在している場合、クエリを拡張して地名のフィールドに対して OR 検索してスコアを加算する
  • 課題2:施設人気度
    • 例:「赤坂サカス」で検索したときに、「スターバックスコーヒー赤坂サカス店」などもヒットするので、「赤坂サカス」自体が埋もれる
    • 施設に人気度や有名度が必要
    • アプローチ1:ツイートを使って人気度付与
      • 同名の施設は、共起語で判定 (北海道の円山公園か、京都の円山公園か)
      • 人名か地名か、説明を用意しておき共起語を用意しておく
    • アプローチ2:位置情報データ(モバイル空間統計)を活用した人気度付与
      • 人口分布の時間変動を把握できるので、人がたくさん集まっているところに高いスコアを付与する
  • 課題3:アプローチ3:パラメータ探索、フィールドごとに重みを設定してチューニングするが少し変えるだけで大きく変わるので難しい
    • 機械学習に寄るパラメータ探索
    • Hyperopt

一言・感想

  • モバイル空間統計 が利用できるのはドコモならではのチートですね!w

AtomicUpdateを50000倍速くした話(SOLR-9592)

ヤフー株式会社 石川 貴大 様


講演内容メモ

  • Atomic Update 部分更新などでよく使われる
  • 一度検索し直すので余り速くない (Realtime Get)
  • 実際に検証すると超遅い (0.1 dps ??)
  • Remote Debug や、コードリーティング、FlameGraph などで調査
  • SlowCompositeReaderWrapper
    • Lucene 的にはセグメントごとに LeafReader を使って個別に検索することが推奨されている
    • が Solr はそれに追いついておらず、SlowCompositeReaderWrapper ではこっそりこれをマージして横断して検索していた
      • SolrIndexSearcher から SlowCompositeReaderWrapper が呼ばれる・・
    • やったことは、LeafReader ごとに検索するように直した
    • v6.3 以降取り込まれる
  • 実は Lucene には一度取ってこなくても更新できる実装が進められている
    • posting が貼れないので検索には使えない

SolrCloud のリカバリー処理

こんにちは!ご無沙汰しております。 この記事は Solr Advent Calendar 2016 の 1日目です!

qiita.com

一日目の出だしにしてはかなり渋め(アドバンスド)な内容かなと思いますが、SolrCloud のリカバリー処理についてコードを読んだり調べてみたので書いてみたいと思います。しかし、特に更新が非常に多い Solr を運用している場合は、最後だけでも是非読んで下さい。読まないと、全台ダウンしちゃいますよ!

今回は、特に文章だらけで申し訳ありませんが、よろしくお願いいたします。

前提

  • SolrCloud をざっと触ったことがあり、複数のレプリカやシャードの構成を組んだり使ったことがあることがある読者を前提に書きます。
  • Solr 5.5 のリカバリー処理について書きます。Solr 6 もそんなに違わないと思いますが。

リカバリー処理とは?

リカバリー処理とは、SolrCloud環境で同一シャード内のレプリカでインデックスデータの同期が失われた場合に、再度同期をとるために Solr によって行われる処理です。 例えば、1シャード3レプリカの3台の構成で、そのうちの1台がダウンしていた場合にインデックスの更新が行われた後、ダウンしていたサーバーが復旧すると、リカバリー処理が行われダウン中に実施されたインデックスの更新が反映されます。 ダウンしていたサーバーが復旧すると Solr Admin の画面で、Recovering となっているとこの処理が行われています。

その前にトランザクションログについて

SolrCloud の構成では、各 Solr のインスタンスごとにトランザクションログ(もしくはアップデートログ、移行 tlog と記載します) を保持しています。この tlog は SolrCloud の Near Realtime Search とデータの確実性を保つために導入されたファイルです。tlog には、データベースのトランザクションログと同様に更新リクエストのインデックス処理前のそのままのデータが記載されています。

SolrCloud ではあるインスタンスに更新データが届くと以下のような処理が行われます。

  1. tlog に書き込む、この際確実にディスクに書き込まれたことを確認する (fsynch)
  2. 書き込まれたことを確認したら、インデックスをメモリ(Java のヒープ上)だけに反映する (soft commit)
  3. (commitされると) メモリ上のインデックスデータを、インデックスのファイルに書き込む (hard commit)

hard commit は、/update?commit=true を送ったりすると行われます。

この時、2 と 3 の hard commit を実行する前にインスタンスが落ちると、インスタンスのヒープ上にしか無い更新データは消えてしまいます。 消えてしまうと困るので、インスタンスが復旧した際に tlog の有無を確認し、あった場合 tlog を再度適用し(リプレイといいます)、インデックスに反映します。ちなみにこの時行われるリプレイでは、いきなりインデックスのファイルに書き込まれます。

これがトランザクションログの役割で、インスタンス1つだけに注目した場合の処理です。以下の記事がこの辺のことを詳細に書いてあるのでご参考に。

lucidworks.com

では、レプリカ1にはインデックスがあって、レプリカ2がディスク障害でインデックスが飛んでしまった場合はどうでしょうか?

リカバリー処理の流れ

インスタンスをまたがったインデックスの障害のケースでも対応できるようにリカバリー処理は作られています。

まず、リカバリーはインスタンスがコレクションに参加した時に必ず行われます。この参加時とは、新規に追加した場合以外にも、ダウンしていたインスタンスが起動した場合も含まれます。この時、リカバリー対象のノードとリーダーノードのデータを比較します。 比較した結果トランザクションログとインデックスに差異がなければ処理は何も行われません。差異があった場合に、実際のリカバリーの処理が行われます。ちなみに、インスタンスを停止してすぐに起動しても一瞬リカバリーに入るのはこのチェックをしているためです。

また、自分自身がリーダーノードである場合も実際の処理は行われません。後述しますが、リーダーノードを正しいデータとしてリカバリーが行われるためです。

対象のノードがフォロワーで、リーダーノードのデータと差異がある場合にのみ、インデックスに対する処理は行われます。実際のリカバリー処理は以下の3ステップで行われます。

1. tlog のリプレイ

まず、tlog がある場合はリプレイします。ここまでは、1台のケースと同じです。

2. PeerSync

次に PeerSync と呼ばれる処理が行われます。

PeerSync とはインスタンスのダウン中に、インデックスの更新リクエストがされ、ダウンしていない同一シャード中の他のサーバーでインデックスが更新された場合に同期するための処理です。ダウンしていたインスタンスでは、PeerSync はリカバリー処理時には必ず実行が可能かチェックされます。具体的な実行が可能かどうかはどのぐらいインデックスに差異があるかを確認し、差異が少なければ PeerSync を行います。

PeerSync はリーダーのノードが保持している tlog と、自分の tlog を比較し大量の差異が無い場合は、行われていない tlog のみをコピーしリプレイを行いインデックスの同期を行います。tlog をコピーするため、tlog の同期も行われます。PeerSync の同期に成功した後はこの後のレプリケーション処理は行いません。

PeerSync が実行可能かどうかのしきい値は、トランザクションログ1ファイルに最大何件保持するかという設定 numRecordsToKeep に依存しています。numRecordsToKeep の件数を 100% とし、自分の新しいドキュメントの新しい方から 20% のインデックスのバージョンが、リーダーの新しい方から 80% のインデックスバージョンよりも古い場合は PeerSync しません。また、逆に自分のインデックスが新しい場合ももちろん PeerSync しません。わかりやすく書くと、単純な更新処理の場合、tlog 1ファイルに保持する最大件数(numRecordsToKeep)が 100 の場合、ダウン中に更新されたドキュメントが 60件未満なら PeerSync 、それ以上ならレプリケーションが行われます。

なぜ、全て PeerSync でリカバリーが行われないかというと、tlog はインデックスの情報ではなく処理前の更新リクエストがそのまま保持されているので、データのサイズが大きくなりがちで、さらに、リプレイ時にインデックス処理が行われるため比較的時間がかかること、tlog には全データを保持していないことなどの理由があると思われます。

3. レプリケーション

PeerSync で同期できない程度にインデックスに差異があった場合に行われるインデックスの同期処理が、レプリケーションです。

レプリケーションではインデックスのバイナリファイルごとリーダーからコピーし、同期をとる処理です。バイナリファイルはインデックスのセグメントごとに分割されており、差異があるセグメントのインデックスファイルのみコピーしインデックスの同期をおこないます。

レプリケーションでは、セグメントごとにわかれたインデックスファイル単位でコピーするため、Optimize などセグメント全体に作り変えが発生する処理を、サーバーのダウン中に行うと全インデックスのコピーが起きるため、レプリケーションに非常に時間がかかるので注意が必要です。ちなみに、レプリケーションではトランザクションログの同期は行われないので、ある程度の件数が新規に更新され、tlog の同期が取れるようになるまでは、ダウンしていたインスタンスを単純に起動・停止するだけでこのレプリケーションまで処理が進み、ファイルはコピーされずに終了します。

また、リカバリー中の更新リクエストはトランザクションログの buffered update という箇所に別途保存され、レプリケーション後にリプレイされます。リプレイ中にも受け取った更新リクエストは buffered update に追記されリプレイされ、全てのリプレイ処理が完了したのちに status が active になります。

何がリカバリー処理のトリガーとなるか?

リカバリー処理のトリガーとなる現象は、インデックスデータに差異が見つかること以外にもいくつかあります。例えば、更新処理時にリーダーからフォロワーにデータが転送されるのですが、この転送が失敗したり、フォロワーでのインデックス処理が失敗したと判断されると、リーダーからフォロワーにリカバリーを行うよう、リクエストが送信されます。

numRecordsToKeep の値に注意!

このリカバリー処理、よく出来ているのですが、更新リクエストが非常に多いユースケースについては注意が必要です。どういうことかというと、気にせずにデフォルト設定で使用すると、リカバリーがいつまでたっても終わらない、ということが起こりえます。

デフォルトの設定ではトランザクションログ1ファイルあたりに保持できるレコードの件数を示す、numRecordsToKeep の設定が 100 しかありません。更新処理が大量にある場合にリカバリーに入ると、ダウン中に発生する更新処理がしきい値を簡単にオーバーするので PeerSync は行われず、レプリケーションに入ります。レプリケーションでは、比較的時間のかかるインデックスのコピー処理が開始されますが、その間にもどんどん更新リクエストが入ってきます。入ってきた更新処理は、リプレイされインデックスに順次反映されますが、コピー中にたまった更新リクエストの数が多いと、リプレイ中にもどんどんトランザクションログに更新リクエストが貯まるので、終わらないという状態になります。

このリカバリー中のインスタンスは検索はできるのですが、検索結果が古く正しくないので、一般的なシステムではリカバリー中のサーバーには検索リクエストを飛ばさないようにすることが多いです。1台がずーっとリカバリー中だと、その分、外のサーバーに検索リクエストの負荷がかかるので、放おって置くと次々にサーバーがダウンし、全台ダウンすることがあります。

なので、numRecordsToKeep の値を大きめに設定することで、できるだけリカバリー処理を PeerSync で済ませることがベスト・プラクティスとなります。ただし、numRecordsToKeep を大きくするとディスクを大量に食うので注意が必要です。

まとめ

numRecordsToKeep の値を大きくするのはもちろんですが、適切な値を決めるには徹底的な負荷検証とチューニングをお忘れなきよう。

明日のアドカレは近藤さん (tkondo) による、更新処理のパフォーマンスに関するないようになりそうですー!乞うご期待。

qiita.com