Solr TolerantUpdateProcessor

こんばんは。
Solr Advent Calender 2017 2日目です。
なお、更新日は12/5となります・・・！

Solr 7の新機能で僕も注目しているReplica Typeについて書こうと思ったのですが既に日本語の記事が有ったので、最近便利だと思った[TolerantUpdateProcessor](https://lucene.apache.org/solr/7_1_0/solr-core/org/apache/solr/update/processor/TolerantUpdateProcessor.html)を紹介します。

# 前提と問題
通常のSolrの更新リクエストは下記のような挙動をします。

ex) 1度のリクエストで100件のドキュメントをインデックスする場合
インデキシングに失敗したドキュメントがあると、その時点で更新処理が中断されそれ以降はインデックスされない。
例えば50件目のドキュメントのインデキシングに失敗すると、1~49件目のドキュメントはインデックスされますが、51件目以降はインデキシング自体が処理されません。

つまり1件分でもデータの書式がおかしくてインデキシングに失敗するドキュメントが存在していると、更新処理が中断されてしまい、最悪の場合大半のドキュメントが更新されていない。
という状況が起こる可能性があります。

対策として1度リクエストでPOSTする件数を10件とかにすれば影響を小さくできますが、それはそれでHTTPリクエストを何度も投げることになり効率が悪いです。


# TolerantUpdateProcessorとは 
この機能は下記の挙動をさせることができます。
ex) 同じく100件を1度に更新させる場合
インデキシングに失敗するとそのドキュメントをスキップし、後続のドキュメントのインデキシングを継続します。
この機能を使うことで、更新に失敗するドキュメントが存在しても影響を小さくできます。
また、ドキュメントが失敗したのかをレスポンスとして受け取ることができるようになります。


# 使い方
solrconfigのupdateRequestProcessorChainに設定を追加します。
余談ですが、LogUpdateProcessorFactory、DistributedUpdateProcessorFactory、RunUpdateProcessorFactoryはupdateRequestProcessorChainを何も指定しない時のデフォルトなので必ず指定しましょう。
(詳しくはSolrCore.javaのloadUpdateProcessorChainsを参照)

```
  <updateRequestProcessorChain name="tolerant">
    <processor class="solr.LogUpdateProcessorFactory" />
    <processor class="solr.TolerantUpdateProcessorFactory">
      <int name="maxErrors">-1</int>
    </processor>
    <processor class="solr.DistributedUpdateProcessorFactory" />
    <processor class="solr.RunUpdateProcessorFactory" />
  </updateRequestProcessorChain>
```

maxErrorsはインデキシングが何件失敗したら処理を中断するかの閾値で、クエリパラメーターで上書き可能です。
しきい値に達すると4xx、5xx系エラーをクライアントに返します。
-1だと無制限になり何件失敗しても処理を継続します。
つまり、更新リクエストのレスポンスステータスは基本的に200が返ることになります。

# サンプルインデックス
## schema.xml
```
<?xml version="1.0" encoding="UTF-8"?>
<schema name="tolerant" version="1.6">
  <fields>
  <field name="id" type="string" indexed="true" stored="true" required="true"></field>
  <field name="_root_" type="string" indexed="true" stored="false"></field>
  <field name="_version_" type="long" indexed="true" stored="true"></field>
  <field name="number" type="int" indexed="true" stored="true" required="true"></field>
  </fields>

<uniqueKey>id</uniqueKey>
  <similarity class="solr.SchemaSimilarityFactory"/>

  <types>
  <fieldType name="binary" class="solr.BinaryField"/>
  <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>
  <fieldType name="booleans" class="solr.BoolField" sortMissingLast="true" multiValued="true"/>
  <fieldType name="currency" class="solr.CurrencyField" currencyConfig="currency.xml" defaultCurrency="USD" precisionStep="8"/>
  <fieldType name="date" class="solr.TrieDateField" positionIncrementGap="0" precisionStep="0"/>
  <fieldType name="double" class="solr.TrieDoubleField" positionIncrementGap="0" precisionStep="0"/>
  <fieldType name="float" class="solr.TrieFloatField" positionIncrementGap="0" precisionStep="0"/>
  <fieldType name="int" class="solr.TrieIntField" positionIncrementGap="0" precisionStep="0"/>
  <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
  <fieldType name="long" class="solr.TrieLongField" positionIncrementGap="0" precisionStep="0"/>
  <fieldType name="point" class="solr.PointType" subFieldSuffix="_d" dimension="2"/>
  <fieldType name="random" class="solr.RandomSortField" indexed="true"/>
  <fieldType name="string" class="solr.StrField" sortMissingLast="true"/>
  <fieldType name="tdate" class="solr.TrieDateField" positionIncrementGap="0" precisionStep="6"/>
  <fieldType name="tdouble" class="solr.TrieDoubleField" positionIncrementGap="0" precisionStep="8"/>
  <fieldType name="tfloat" class="solr.TrieFloatField" positionIncrementGap="0" precisionStep="8"/>
  <fieldType name="tint" class="solr.TrieIntField" positionIncrementGap="0" precisionStep="8"/>
  <fieldType name="tlong" class="solr.TrieLongField" positionIncrementGap="0" precisionStep="8"/>
  </types>


</schema>
```

## データ
```
[
  {
    "id":"1",
    "number":1
  },
  {
    "id":"2",
    "number":2,
    "aaa":"aaa"
  },
  {
    "id":"3",
    "number":3,
    "aaa":"aaa"
  },
  {
    "id":"4",
    "number":4
  }
]
```


# 具体例
## TolerantUpdateProcessor無し
インデックスは0件であることが分かります。

```
$ curl -H 'Content-type: application/json' 'http://localhost:8080/solr/test/update?indent=true&commit=true' -d @doc
{
  "responseHeader":{
    "status":400,
    "QTime":130},
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"ERROR: [doc=2] unknown field 'aaa'",
    "code":400}}

$curl 'http://localhost:8080/solr/test/select?indent=on&q=*:*&wt=json"
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"on",
      "wt":"json",
      "_":"1512404574970"}},
  "response":{"numFound":0,"start":0,"docs":[]
  }}
```

## TolerantUpdateProcessor有り
失敗したドキュメントのユニークキーとエラーメッセージが出ています。

```
$ curl -H 'Content-type: application/json' 'http://localhost:8080/solr/test/update?indent=true&commit=true&update.chain=tolerant' -d @doc
{
  "responseHeader":{
    "errors":[{
        "type":"ADD",
        "id":"2",
        "message":"ERROR: [doc=2] unknown field 'aaa'"},
      {
        "type":"ADD",
        "id":"3",
        "message":"ERROR: [doc=3] unknown field 'aaa'"}],
    "maxErrors":2,
    "status":0,
    "QTime":153}}
```

インデックスにも入っています。

```
$curl 'http://localhost:8080/solr/test/select?indent=on&q=*:*&wt=json"
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"on",
      "wt":"json",
      "_":"1512404858578"}},
  "response":{"numFound":2,"start":0,"docs":[
      {
        "id":"1",
        "number":1,
        "_version_":1585871458610970624},
      {
        "id":"4",
        "number":4,
        "_version_":1585871458619359232}]
  }}
```

# 注意
* 基本的に200が返ることになる(絶対ではありません)ので、レスポンスをパースしてエラーハンドリングすることになります
* 大量のドキュメントの更新が失敗すると、レスポンスサイズが大変なことになる可能性もあります