部分ドキュメント更新

Solrインデックスに必要なコンテンツのインデックスを作成したら、それらのドキュメントへの変更に対処するための戦略について考え始めるでしょう。Solrは、部分的にしか変更されていないドキュメントを更新するための3つのアプローチをサポートしています。

1つ目はアトミック更新です。このアプローチでは、ドキュメント全体を再インデックスすることなく、ドキュメントの1つ以上のフィールドのみを変更できます。

2つ目のアプローチはインプレース更新として知られています。このアプローチはアトミック更新に似ていますが(ある意味ではアトミック更新のサブセットです)、単一値のインデックスなしで非ストアのdocValueベースの数値フィールドを更新する場合にのみ使用できます。

3つ目のアプローチは、楽観的同時実行または楽観的ロックとして知られています。これは多くのNoSQLデータベースの機能であり、バージョンに基づいてドキュメントを条件付きで更新できます。このアプローチには、バージョンの一致または不一致に対処する方法に関するセマンティクスとルールが含まれています。

アトミック更新(およびインプレース更新)と楽観的同時実行は、ドキュメントの変更を管理するための独立した戦略として使用することも、組み合わせることもできます。楽観的同時実行を使用して、アトミック更新を条件付きで適用できます。

アトミック更新

Solrは、ドキュメントの値をアトミックに更新するいくつかの修飾子をサポートしています。これにより、特定のフィールドのみを更新できるため、インデックスの追加速度がアプリケーションにとって重要な環境で、インデックス作成プロセスを高速化できます。

アトミック更新を使用するには、更新する必要があるフィールドに修飾子を追加します。コンテンツは更新、追加、またはフィールドが数値型の場合は、増分的または減分的に増減できます。

set

指定された値でフィールド値を設定または置換するか、新しい値として 'null' または空のリストが指定されている場合は値を削除します。

単一の値として、またはmultiValuedフィールドのリストとして指定できます。

add

指定された値をmultiValuedフィールドに追加します。単一の値として、またはリストとして指定できます。

add-distinct

指定された値を、まだ存在しない場合にのみ、multiValuedフィールドに追加します。単一の値またはリストとして指定できます。

削除

指定された値を、multiValuedフィールドから(すべての出現箇所を)削除します。単一の値またはリストとして指定できます。

削除正規表現

指定された正規表現のすべての出現箇所を、multiValuedフィールドから削除します。単一の値またはリストとして指定できます。

インクリメント

数値フィールドの値を、指定された整数または浮動小数点数で増減します。正の値はフィールドの値を増やし、負の値は減らします。

フィールドストレージ

ドキュメントをアトミックに更新するコア機能では、スキーマ内のすべてのフィールドが、<copyField/>の宛先であるフィールドを除いて、保存済(stored="true")またはdocValues(docValues="true")として構成されている必要があります。<copyField/>の宛先は、stored="false"と、docValues="false"またはuseDocValuesAsStored="false"のいずれかとして構成する必要があります。アトミック更新は、既存の保存済フィールド値によって表されるドキュメントに適用されます。copyFieldの宛先フィールドのすべてのデータは、copyFieldのソースのみから発生する必要があります。

<copyField/>の宛先が保存済として構成されている場合、Solrはフィールドの現在の値と、ソースフィールドからの追加コピーの両方をインデックス化しようとします。そのようなフィールドに、インデックス作成プログラムからの情報とcopyFieldからの情報が含まれている場合、アトミック更新が行われると、インデックス作成プログラムから元々来た情報が失われます。

<copyField/>の宛先について上記で述べたように、保存されないように設定する必要がある、派生フィールドの他の種類もあります。BBoxFieldやLatLonSpatialFieldTypeなど、いくつかの空間フィールドタイプは派生フィールドを使用します。CurrencyFieldTypeも派生フィールドを使用します。これらのタイプは、通常、動的フィールド定義によって指定される追加のフィールドを作成します。その動的フィールド定義は保存されていない必要があり、そうでない場合、インデックス作成は失敗します。

ドキュメントの一部を更新する例

コレクションに次のドキュメントが存在する場合

{"id":"mydoc",
 "price":10,
 "popularity":42,
 "categories":["kids"],
 "sub_categories":["under_5","under_10"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

次の更新コマンドを適用すると

{"id":"mydoc",
 "price":{"set":99},
 "popularity":{"inc":-7},
 "categories":{"add":["toys","games"]},
 "sub_categories":{"add-distinct":"under_10"},
 "promo_ids":{"remove":"a123x"},
 "tags":{"remove":["free_to_try","on_sale"]}
}

コレクション内の結果のドキュメントは次のようになります

{"id":"mydoc",
 "price":99,
 "popularity":35,
 "categories":["kids","toys","games"],
 "sub_categories":["under_5","under_10"],
 "tags":["buy_now","clearance"]
}

子ドキュメントの更新

Solrは、アトミック更新の一部として子ドキュメントの変更、追加、削除をサポートしています。構文的には、ドキュメントの子を変更する更新は、以下の例で示すように、単純なフィールドの通常の原子更新と非常に似ています。

子ドキュメントを更新するためのスキーマと構成の要件は、上記の原子更新のフィールドストレージ要件と同じです。

Solrは、内部的には、ネストされたドキュメントとネストされていないドキュメントに対して同様に概念的に動作します。スタンドアロンドキュメントではなく、ネストされたドキュメントの(ルートからの)ツリー全体に適用されるだけです。このため、より多くのオーバーヘッドが発生することが予想されます。インプレース更新はそれを回避します。

SolrCloudで子ドキュメントIDを使用して更新をルーティングする

SolrCloudがドキュメントの更新を受信すると、コレクションのドキュメントルーティングルールを使用して、ドキュメントのidに基づいて、どのシャードが更新を処理する必要があるかを決定します。

子ドキュメントidを指定する更新を送信する場合、これはデフォルトでは機能しません。ドキュメントを送信する正しいシャードは、更新される子ドキュメントのidではなく、子ドキュメントが含まれるブロックの「ルート」ドキュメントのidに基づいています。

Solrはこれに対処するための2つのソリューションを提供しています

  • クライアントは、Solrにどのシャードが更新を処理する必要があるかを伝えるために、各更新で、パラメーター値としてルートドキュメントのidを持つ_route_パラメーターを指定できます。

  • クライアントは、(デフォルトの)compositeIdルーターの「プレフィックスルーティング」機能を使用して、すべてのドキュメントをインデックス化するときに、ブロック内のすべての子/子孫ドキュメントがルートレベルドキュメントと同じidプレフィックスを使用するようにできます。これにより、Solrのデフォルトのルーティングロジックが、子ドキュメントの更新を正しいシャードに自動的に送信します。

さらに、この部分的な更新の_root_フィールドでルートドキュメントのIDを必ず指定する必要があります。これが、Solrがルートドキュメントではなく、子ドキュメントを更新していることを理解する方法です。

以下のすべての例ではidプレフィックスを使用しているため、これらの例では_route_パラメーターは必要ありません。

今後の例では、ネストされたドキュメントのインデックス作成でカバーされているドキュメントと同じドキュメントを含むインデックスを想定します。

[{ "id": "P11!prod",
   "name_s": "Swingline Stapler",
   "description_t": "The Cadillac of office staplers ...",
   "skus": [ { "id": "P11!S21",
               "color_s": "RED",
               "price_i": 42,
               "manuals": [ { "id": "P11!D41",
                              "name_s": "Red Swingline Brochure",
                              "pages_i":1,
                              "content_t": "..."
                            } ]
             },
             { "id": "P11!S31",
               "color_s": "BLACK",
               "price_i": 3
             } ],
   "manuals": [ { "id": "P11!D51",
                  "name_s": "Quick Reference Guide",
                  "pages_i":1,
                  "content_t": "How to use your stapler ..."
                },
                { "id": "P11!D61",
                  "name_s": "Warranty Details",
                  "pages_i":42,
                  "content_t": "... lifetime guarantee ..."
                } ]
 },
 { "id": "P22!prod",
   "name_s": "Mont Blanc Fountain Pen",
   "description_t": "A Premium Writing Instrument ...",
   "skus": [ { "id": "P22!S22",
               "color_s": "RED",
               "price_i": 89,
               "manuals": [ { "id": "P22!D42",
                              "name_s": "Red Mont Blanc Brochure",
                              "pages_i":1,
                              "content_t": "..."
                            } ]
             },
             { "id": "P22!S32",
               "color_s": "BLACK",
               "price_i": 67
             } ],
   "manuals": [ { "id": "P22!D52",
                  "name_s": "How To Use A Pen",
                  "pages_i":42,
                  "content_t": "Start by removing the cap ..."
                } ]
 } ]

子ドキュメントフィールドの変更

上記のすべてのアトミック更新操作は、子ドキュメントの「実際の」フィールドでサポートされています

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P11!S31",
  "_root_": "P11!prod",
  "price_i": { "inc": 73 },
  "color_s": { "set": "GREY" }
} ]'

すべての子ドキュメントの置換

通常の(multiValued)フィールドと同様に、setキーワードを使用して、疑似フィールド内のすべての子ドキュメントを置換できます

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P22!S22",
  "_root_": "P22!prod",
  "manuals": { "set": [ { "id": "P22!D77",
                          "name_s": "Why Red Pens Are the Best",
                          "content_t": "... correcting papers ...",
                        },
                        { "id": "P22!D88",
                          "name_s": "How to get Red ink stains out of fabric",
                          "content_t": "... vinegar ...",
                        } ] }

} ]'

子ドキュメントの追加

通常の(multiValued)フィールドと同様に、addキーワードを使用して、追加の子ドキュメントを疑似フィールドに追加できます

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P11!S21",
  "_root_": "P11!prod",
  "manuals": { "add": { "id": "P11!D99",
                        "name_s": "Why Red Staplers Are the Best",
                        "content_t": "Once upon a time, Mike Judge ...",
                      } }
} ]'

これは(IDによる)追加または置換であることに注意してください。つまり、ドキュメントP11!S21に、IDP11!D99(追加しているもの)を持つ子ドキュメントが既にある場合、それは置換されます。

子ドキュメントの削除

通常の(multiValued)フィールドと同様に、removeキーワードを使用して、その疑似フィールドから(idによって)子ドキュメントを削除できます

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P11!S21",
  "_root_": "P11!prod",
  "manuals": { "remove": { "id": "P11!D41" } }
} ]'

インプレース更新

インプレース更新は、アトミック更新と非常によく似ています。ある意味では、これはアトミック更新のサブセットです。通常のアトミック更新では、更新の適用中にドキュメント全体が内部的に再インデックス化されます。ただし、このアプローチでは、更新されるフィールドのみが影響を受け、ドキュメントの残りの部分は内部的に再インデックス化されません。したがって、インプレース更新の効率は、更新されるドキュメントのサイズ(つまり、フィールド数、フィールドサイズなど)の影響を受けません。効率のこれらの内部的な違いを除けば、アトミック更新とインプレース更新の間に機能的な違いはありません。

アトミック更新操作は、更新するフィールドが次の3つの条件を満たす場合にのみ、このインプレースアプローチを使用して実行されます

  • インデックス化されず(indexed="false")、保存されておらず(stored="false")、単一の値(multiValued="false")の数値docValues(docValues="true")フィールドである。

  • _version_フィールドも、インデックス化されず、保存されておらず、単一の値のdocValuesフィールドである。

  • 更新されたフィールドのコピーターゲットがある場合、それらも、インデックス化されず、保存されておらず、単一の値の数値docValuesフィールドである。

インプレース更新を使用するには、更新する必要があるフィールドに修飾子を追加します。コンテンツは、更新または増減できます。

set

フィールド値を指定された値で設定または置換します。単一の値として指定できます。

インクリメント

数値フィールドの値を、指定された整数または浮動小数点数で増減します。正の値はフィールドの値を増やし、負の値は減らします。

インプレースで実行できないアトミック更新の防止

更新をインプレースで実行できることを保証するために必要なすべての条件が満たされていることを確認するのが難しい場合があるため、Solrはupdate.partial.requireInPlaceという名前のリクエストパラメーターオプションをサポートしています。trueに設定すると、インプレースで実行できないアトミック更新は失敗します。ユーザーは、更新リクエストがインプレースで実行できない場合に「すぐに失敗」することを希望する場合、このオプションを指定できます。

インプレース更新の例

価格と人気度フィールドがスキーマで次のように定義されている場合

<field name="price" type="float" indexed="false" stored="false" docValues="true"/>

<field name="popularity" type="float" indexed="false" stored="false" docValues="true"/>

コレクションに次のドキュメントが存在する場合

{
 "id":"mydoc",
 "price":10,
 "popularity":42,
 "categories":["kids"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

次の更新コマンドを適用すると

{
 "id":"mydoc",
 "price":{"set":99},
 "popularity":{"inc":20}
}

コレクション内の結果のドキュメントは次のようになります

{
 "id":"mydoc",
 "price":99,
 "popularity":62,
 "categories":["kids"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

楽観的同時実行制御

楽観的同時実行制御は、ドキュメントを更新/置換するクライアントアプリケーションが、置換/更新するドキュメントが別のクライアントアプリケーションによって同時に変更されていないことを確認するために使用できるSolrの機能です。この機能は、インデックス内のすべてのドキュメントに_version_フィールドを必要とし、それを更新コマンドの一部として指定された_version_と比較することによって機能します。デフォルトでは、Solrのスキーマには_version_フィールドが含まれており、このフィールドは新しいドキュメントごとに追加されます。

一般に、楽観的同時実行制御の使用には、次のワークフローが含まれます

  1. クライアントがドキュメントを読み取ります。Solrでは、最新バージョンを確実に入手するために、/getハンドラーでドキュメントを取得する場合があります。

  2. クライアントはローカルでドキュメントを変更します。

  3. クライアントは、たとえば、/updateハンドラーを使用して、変更されたドキュメントをSolrに再送信します。

  4. バージョンの競合(HTTPエラーコード409)がある場合、クライアントはプロセスを最初からやり直します。

クライアントが変更されたドキュメントをSolrに再送信するときに、_version_を更新に含めて、楽観的同時実行制御を呼び出すことができます。ドキュメントをいつ更新する必要があるか、またはいつ競合を報告する必要があるかを定義するために、特定のセマンティクスが使用されます。

  • _version_フィールドのコンテンツが「1」より大きい場合(つまり、「12345」)、ドキュメント内の_version_はインデックス内の_version_と一致する必要があります。

  • _version_フィールドのコンテンツが「1」に等しい場合、ドキュメントは単に存在する必要があります。この場合、バージョンマッチングは発生しませんが、ドキュメントが存在しない場合、更新は拒否されます。

  • _version_フィールドのコンテンツが「0」未満(つまり、「-1」)の場合、ドキュメントは存在しない必要があります。この場合、バージョンマッチングは発生しませんが、ドキュメントが存在する場合、更新は拒否されます。

  • _version_フィールドのコンテンツが「0」に等しい場合、バージョンが一致するかどうか、またはドキュメントが存在するかどうかは関係ありません。存在する場合、上書きされます。存在しない場合、追加されます。

ドキュメントが一括で追加/更新される場合、1つのバージョンの競合でも、バッチ全体が拒否される可能性があります。バッチ内の一部のドキュメントでバージョンの制約が失敗した場合に、バッチ全体の失敗を回避するには、パラメーターfailOnVersionConflicts=falseを使用します。

更新されるドキュメントに_version_フィールドが含まれておらず、アトミック更新が使用されていない場合、ドキュメントは通常のSolrルールによって処理されます。通常、これは前のバージョンを破棄することです。

オプティミスティック同時実行制御を使用する場合、クライアントはオプションのversions=trueリクエストパラメータを含めることで、追加されるドキュメントの新しいバージョンをレスポンスに含めるように指示できます。これにより、クライアントは、冗長な/getリクエストを行う必要なく、追加されたすべてのドキュメントの_version_をすぐに知ることができます。

以下に、クエリでversions=trueを使用するいくつかの例を示します。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?versions=true&omitHeader=true' --data-binary '
[ { "id" : "aaa" },
  { "id" : "bbb" } ]'
{
  "adds":[
    "aaa",1632740120218042368,
    "bbb",1632740120250548224]}

この例では、2つのドキュメント「aaa」と「bbb」を追加しました。リクエストにversions=trueを追加したため、レスポンスには各ドキュメントのドキュメントバージョンが表示されます。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?_version_=999999&versions=true&omitHeader=true' --data-binary '
  [{ "id" : "aaa",
     "foo_s" : "update attempt with wrong existing version" }]'
{
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"version conflict for aaa expected=999999 actual=1632740120218042368",
    "code":409}}

この例では、ドキュメント「aaa」を更新しようとしましたが、リクエストで間違ったバージョンを指定しました。version=999999は、ドキュメントを追加したときに取得したドキュメントバージョンと一致しません。レスポンスでエラーが発生します。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?_version_=1632740120218042368&versions=true&commit=true&omitHeader=true' --data-binary '
[{ "id" : "aaa",
   "foo_s" : "update attempt with correct existing version" }]'
{
  "adds":[
    "aaa",1632740462042284032]}

次に、インデックスの値と一致する_version_の値を含む更新を送信すると、成功します。更新リクエストにversions=trueを含めたため、レスポンスには_version_フィールドの異なる値が含まれます。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?&versions=true&commit=true&omitHeader=true' --data-binary '
[{ "id" : "aaa", _version_ : 100,
   "foo_s" : "update attempt with wrong existing version embedded in document" }]'
{
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"version conflict for aaa expected=100 actual=1632740462042284032",
    "code":409}}

次に、ドキュメント自体に埋め込まれた_version_の値を含む更新を送信しました。このリクエストは、間違ったバージョンを指定したため失敗します。これは、ドキュメントがバッチで送信され、ドキュメントごとに異なる_version_値を指定する必要がある場合に役立ちます。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?&versions=true&commit=true&omitHeader=true' --data-binary '
[{ "id" : "aaa", _version_ : 1632740462042284032,
   "foo_s" : "update attempt with correct version embedded in document" }]'
{
  "adds":[
    "aaa",1632741942747987968]}

次に、ドキュメント自体に埋め込まれた_version_の値を含む更新を送信しました。このリクエストは、間違ったバージョンを指定したため失敗します。これは、ドキュメントがバッチで送信され、ドキュメントごとに異なる_version_値を指定する必要がある場合に役立ちます。

$ curl 'https://127.0.0.1:8983/solr/techproducts/query?q=*:*&fl=id,_version_&omitHeader=true'
{
  "response":{"numFound":3,"start":0,"docs":[
      { "_version_":1632740120250548224,
        "id":"bbb"},
      { "_version_":1632741942747987968,
        "id":"aaa"}]
  }}

最後に、レスポンスに_version_フィールドを含めるように要求するクエリを発行できます。例のインデックスにある2つのドキュメントに対してそれを見ることができます。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?versions=true&_version_=-1&failOnVersionConflicts=false&omitHeader=true' --data-binary '
[ { "id" : "aaa" },
  { "id" : "ccc" } ]'
{
  "adds":[
    "ccc",1632740949182382080]}

この例では、2つのドキュメント「aaa」と「ccc」を追加しました。パラメータ_version_=-1を指定したため、このリクエストは、すでに存在するため、ID aaaのドキュメントを追加するべきではありません。failOnVersionConflicts=falseパラメータが指定されているため、リクエストは成功し、エラーは発生しません。レスポンスは、ドキュメントcccのみが追加され、aaaは黙って無視されたことを示しています。

詳細については、Apache Lucene EuroCon 2012のYonik SeeleyによるSolr 4のNoSQL機能に関するプレゼンテーションも参照してください。

ドキュメント中心のバージョニング制約

オプティミスティック同時実行制御は非常に強力で、_version_フィールドに内部的に割り当てられたグローバルに一意の値を使用しているため、非常に効率的に動作します。ただし、場合によっては、ユーザーが独自のドキュメント固有のバージョンフィールドを構成し、バージョン値が外部システムによってドキュメントごとに割り当てられ、「古い」バージョンでドキュメントを置き換えようとする更新をSolrに拒否させたい場合があります。このような状況では、DocBasedVersionConstraintsProcessorFactoryが役立ちます。

DocBasedVersionConstraintsProcessorFactoryの基本的な使用法は、UpdateRequestProcessorChainの一部としてsolrconfig.xmlで構成し、更新を検証するときにチェックする必要があるスキーマのカスタムversionFieldの名前を指定することです。

<processor class="solr.DocBasedVersionConstraintsProcessorFactory">
  <str name="versionField">my_version_l</str>
</processor>

versionFieldは、バージョン番号をチェックするフィールドのカンマ区切りリストであることに注意してください。構成が完了すると、この更新プロセッサは、既存のドキュメントのmy_version_lフィールドの値が、「新しい」ドキュメントの値よりも大きくない場合に、既存のドキュメントを更新しようとする試みをすべて拒否します(HTTPエラーコード409)。

versionFieldと_version_

Solrが通常のオプティミスティック同時実行制御に使用する_version_フィールドには、SolrCloudのレプリカへの更新の分散方法に関する重要なセマンティクスもあり、Solrによって内部的に割り当てる必要があります。ユーザーはそのフィールドを再利用して、DocBasedVersionConstraintsProcessorFactory構成で使用するためのversionFieldとして指定することはできません。

DocBasedVersionConstraintsProcessorFactoryは、以下の追加の構成パラメータをサポートしています。これらはすべてオプションです。

ignoreOldUpdates

オプション

デフォルト:false

trueに設定すると、versionFieldが小さすぎる場合の更新を拒否する代わりに、更新は黙って無視されます(クライアントにステータス200を返します)。

deleteVersionParam

オプション

デフォルト:なし

このプロセッサがIDによる削除コマンドも検査する必要があることを示すために指定できる文字列パラメータです。

このオプションの値は、プロセッサがすべてのIDによる削除試行に必須と見なし、クライアントが削除対象のドキュメントの既存の値よりも大きいversionFieldの値を指定するために使用する必要があるリクエストパラメータの名前である必要があります。

このリクエストパラメータを使用する場合、成功するのに十分なドキュメントバージョン番号を持つIDによる削除コマンドはすべて、内部的に、削除されたバージョンの記録を保持するために、一意キーとversionFieldを除いて空の新しいドキュメントで既存のドキュメントを置き換えるAdd Documentコマンドに変換されます。これにより、将来のAdd Documentコマンドが、その「新しい」バージョンが十分に高くない場合に失敗します。

versionFieldがリストとして指定されている場合、このパラメータも、パラメータがフィールドに対応するように、同じサイズのカンマ区切りリストとして指定する必要があります。

supportMissingVersionOnOldDocs

オプション

デフォルト:false

trueに設定すると、この機能が有効になるに書き込まれ、versionFieldがないドキュメントを上書きできます。

追加情報と使用例については、DocBasedVersionConstraintsProcessorFactory javadocsおよびテスト用のsolrconfig.xmlファイルを参照してください。