部分文件更新

一旦您在 Solr 索引中索引了所需的內容,您就會想要開始思考處理這些文件變更的策略。Solr 支援三種更新僅部分變更文件的方法。

第一種是原子更新。此方法允許僅變更文件的一個或多個欄位,而無需重新索引整個文件。

第二種方法稱為就地更新。此方法類似於原子更新(在某種意義上是原子更新的子集),但只能用於更新單值的非索引和非儲存、基於 docValue 的數值欄位。

第三種方法稱為樂觀並行樂觀鎖定。它是許多 NoSQL 資料庫的功能,允許根據文件的版本有條件地更新文件。此方法包含如何處理版本符合或不符合的語意和規則。

原子更新(和就地更新)以及樂觀並行可以用作管理文件變更的獨立策略,或者它們可以組合使用:您可以使用樂觀並行來有條件地應用原子更新。

原子更新

Solr 支援多個原子更新文件值的修飾詞。這允許僅更新特定欄位,這有助於加速索引新增速度對應用程式至關重要的環境中的索引處理。

若要使用原子更新,請將修飾詞新增至需要更新的欄位。可以更新、新增內容,或者如果欄位具有數值類型,則可以遞增或遞減內容。

set

使用指定的值設定或取代欄位值,或如果將 'null' 或空清單指定為新值,則移除值。

可以指定為單一值,或對於多值欄位,指定為清單。

add

將指定的值新增至多值欄位。可以指定為單一值,或指定為清單。

add-distinct

僅當多值欄位中尚未存在指定的值時,才將其新增至多值欄位。可以指定為單一值,或指定為清單。

remove

從多值欄位中移除(所有出現的)指定值。可以指定為單一值,或指定為清單。

removeregex

從多值欄位中移除所有出現的指定正規表示式。可以指定為單一值,或指定為清單。

inc

依特定量遞增或遞減數值欄位的值,指定為單一整數或浮點數。正數會遞增欄位的值,而負數會遞減。

欄位儲存

原子更新文件的核心功能需要將結構描述中的所有欄位設定為已儲存 (stored="true") 或 docValues (docValues="true"),但作為 <copyField/> 目標的欄位除外,這些欄位必須設定為 stored="false",且必須設定為 docValues="false"useDocValuesAsStored="false"。原子更新會套用至現有儲存欄位值所代表的文件。copyField 目標欄位中的所有資料都必須僅來自 copyField 來源。

如果 <copyField/> 的目標欄位設定為儲存 (stored),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 支援在原子更新中修改、新增和移除子文件。語法上,更改文件子項的更新與簡單欄位的常規原子更新非常相似,如下面的範例所示。

更新子文件的 Schema 和設定要求,與上面提到的原子更新的欄位儲存要求相同。

在底層,Solr 對巢狀文件的行為概念上與非巢狀文件類似,只是它適用於整個巢狀文件樹(從根開始),而不是單獨的文件。您可以預期會有更多的開銷,因為這個原因。就地更新可以避免這種情況。

在 SolrCloud 中使用子文件 ID 路由更新

當 SolrCloud 接收到文件更新時,會使用集合的文件路由規則,根據文件的 id 來判斷哪個分片應該處理更新。

當發送指定子文件 id 的更新時,這預設情況下不會生效:發送文件的正確分片是基於子文件所在區塊的「根」文件的 id,**而不是**正在更新的子文件的 id

Solr 提供了兩種解決方案來解決這個問題

  • 用戶端可以在每次更新時指定一個_route_ 參數,並以根文件的 id 作為參數值,來告知 Solr 哪個分片應該處理更新。

  • 用戶端可以在索引所有文件時使用 (預設) 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)。也就是說,如果 doc P11!S21 碰巧已經有一個 ID 為 P11!D99 的子 doc (我們要新增的那個),那麼它將被取代。

移除子文件

與普通的 (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" } }
} ]'

就地更新

就地更新與原子更新非常相似;在某種意義上,這是原子更新的一個子集。在常規的原子更新中,整個文件在更新應用期間會在內部重新索引。然而,在這種方法中,只有要更新的欄位會受到影響,文件的其餘部分不會在內部重新索引。因此,就地更新的效率不受更新文件的大小 (即欄位數量、欄位大小等) 的影響。除了效率上的這些內部差異之外,原子更新和就地更新之間沒有功能上的差異。

只有當要更新的欄位滿足以下三個條件時,才會使用這種就地方法執行原子更新操作

  • 是非索引 (indexed="false")、非儲存 (stored="false")、單值 (multiValued="false") 數值 docValues (docValues="true") 欄位;

  • _version_ 欄位也是非索引、非儲存的單值 docValues 欄位;並且,

  • 已更新欄位的複製目標(如果有的話)也是非索引、非儲存的單值數值 docValues 欄位。

若要使用就地更新,請將修飾符新增到需要更新的欄位。可以更新或增加/減少內容。

set

使用指定的值設定或取代欄位值。可以指定為單個值。

inc

將數值欄位的值增加或減少特定數量,指定為單個整數或浮點數。正數會增加欄位的值,負數會減少欄位的值。

防止無法就地完成的原子更新

由於要確保滿足所有必要的條件以確保可以進行就地更新可能很棘手,因此 Solr 支援名為 update.partial.requireInPlace 的請求參數選項。當設定為 true 時,無法就地完成的原子更新將會失敗。如果使用者希望更新請求在無法就地完成時「快速失敗」,則可以指定此選項。

就地更新範例

如果 price 和 popularity 欄位在 schema 中定義為

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

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

啟用 Doc Values

對於版本 >= 1.7 的 schema,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 的 Schema 包含 _version_ 欄位,並且此欄位會自動新增到每個新文件中。

一般來說,使用樂觀並行涉及以下工作流程

  1. 用戶端讀取文件。在 Solr 中,可以使用 /get 處理程序檢索文件,以確保擁有最新版本。

  2. 用戶端在本地更改文件。

  3. 用戶端將更改的文件重新提交給 Solr,例如,可以使用 /update 處理程序。

  4. 如果出現版本衝突 (HTTP 錯誤代碼 409),用戶端會重新啟動該過程。

當用戶端將更改的文件重新提交給 Solr 時,可以將 _version_ 與更新一起包含,以調用樂觀並行控制。使用特定的語義來定義何時應該更新文件或何時報告衝突。

  • 如果 _version_ 欄位中的內容大於 '1' (即 '12345'),則文件中的 _version_ 必須與索引中的 _version_ 相符。

  • 如果 _version_ 欄位中的內容等於 '1',則該文件必須存在。在這種情況下,不會發生版本匹配,但如果該文件不存在,則更新將被拒絕。

  • 如果 _version_ 欄位中的內容小於 '0' (即 '-1'),則該文件必須存在。在這種情況下,不會發生版本匹配,但如果該文件存在,則更新將被拒絕。

  • 如果 _version_ 欄位中的內容等於 '0',那麼版本是否匹配或者該文件是否存在都沒關係。如果它存在,它將被覆蓋;如果它不存在,它將被新增。

當批量新增/更新文件時,即使是單個版本衝突也可能導致拒絕整個批次。當批次中一個或多個文件的版本約束失敗時,請使用參數 failOnVersionConflicts=false 來避免整個批次的失敗。

如果要更新的文件不包含 _version_ 欄位,且未使用原子更新,則該文件將按照正常的 Solr 規則處理,通常是丟棄之前的版本。

使用樂觀並行時,用戶端可以包含一個可選的 versions=true 請求參數,以指示應在回應中包含正在新增文件的版本。這允許用戶端立即知道每個新增文件的 _version_ 是什麼,而無需發出冗餘的 /get 請求

以下是一些在查詢中使用 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_ 欄位的查詢,我們可以看到範例索引中的兩個文件。

$ 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 則被靜默忽略。

有關更多資訊,請參閱 Yonik Seeley 在 Apache Lucene EuroCon 2012 上的Solr 4 中的 NoSQL 功能簡報。

以文件為中心的版本控制限制

樂觀並行非常強大,並且運作非常有效,因為它為 _version_ 欄位使用內部指定的、全域唯一的值。但是,在某些情況下,使用者可能想要配置他們自己的文件特定版本欄位,其中版本值由外部系統以每個文件為基礎進行指定,並讓 Solr 拒絕嘗試用「較舊」版本取代文件的更新。在這種情況下,DocBasedVersionConstraintsProcessorFactory 可能會很有用。

DocBasedVersionConstraintsProcessorFactory 的基本用法是在 solrconfig.xml 中將其配置為 UpdateRequestProcessorChain 的一部分,並指定您的 schema 中自訂的 versionField 名稱,該欄位將在驗證更新時進行檢查。

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

請注意,versionField 是一個以逗號分隔的欄位列表,用於檢查版本號。一旦配置完成,此更新處理器將拒絕 (HTTP 錯誤碼 409) 任何嘗試更新現有文件的操作,如果「新」文件中 my_version_l 欄位的值不大于現有文件中該欄位的值。

versionField 與 _version_ 的比較

Solr 用於其正常樂觀並發的 _version_ 欄位在更新如何分發到 SolrCloud 中的副本方面也具有重要的語義,並且必須由 Solr 內部指派。使用者不能重新使用該欄位並將其指定為 DocBasedVersionConstraintsProcessorFactory 配置中使用的 versionField

DocBasedVersionConstraintsProcessorFactory 支援以下額外的配置參數,這些參數都是可選的

ignoreOldUpdates

可選

預設值:false

如果設定為 true,則會靜默忽略更新 (並向客戶端返回狀態碼 200),而不是拒絕 versionField 值過低的更新。

deleteVersionParam

可選

預設值:無

可以指定一個字串參數,以指示此處理器也應檢查依 ID 刪除 (Delete By Id) 命令。

此選項的值應該是一個請求參數的名稱,該參數對於所有依 ID 刪除的嘗試都是強制性的,並且必須由客戶端使用,以指定 versionField 的值,該值大於要刪除的現有文件的值。

當使用此請求參數時,任何具有足夠高的文件版本號以成功刪除的依 ID 刪除命令都將在內部轉換為新增文件 (Add Document) 命令,該命令將使用一個新的空文件替換現有文件,除了 Unique Key 和 versionField 之外,以記錄已刪除的版本,以便如果未來的新增文件命令的「新」版本不夠高,則會失敗。

如果 versionField 被指定為列表,則此參數也必須被指定為相同大小的逗號分隔列表,以便參數與欄位對應。

supportMissingVersionOnOldDocs

可選

預設值:false

如果設定為 true,允許任何在啟用此功能之前寫入且缺少 versionField 的文件被覆蓋。

請參閱 DocBasedVersionConstraintsProcessorFactory javadocs測試 solrconfig.xml 檔案,以獲取其他資訊和範例用法。