部分文档更新

在 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/> 目标配置为存储,则 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 提供了两种解决方案来解决此问题

  • 客户端可以在每次更新时指定一个_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 'http://localhost: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" }
} ]'

替换所有子文档

与普通(多值)字段一样,set关键字可用于替换伪字段中的所有子文档

curl -X POST 'http://localhost: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 ...",
                        } ] }

} ]'

添加子文档

与普通(多值)字段一样,add关键字可用于将其他子文档添加到伪字段

curl -X POST 'http://localhost: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 碰巧已经有一个 ID 为 P11!D99(我们要添加的 ID)的子文档,那么它将被替换。

删除子文档

与普通(多值)字段一样,remove 关键字可用于从其伪字段中删除子文档(按 id)。

curl -X POST 'http://localhost: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 时,无法就地完成的原子更新将失败。如果用户希望在无法就地完成更新请求时更新请求“快速失败”,则可以指定此选项。

就地更新示例

如果价格和流行度字段在模式中定义为

<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. 客户端将已更改的文档重新提交到 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' 'http://localhost: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' 'http://localhost: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' 'http://localhost: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' 'http://localhost: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' 'http://localhost: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 'http://localhost: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' 'http://localhost: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 的一部分,并在架构中指定自定义 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 内部分配。用户无法重新利用该字段并将其指定为 versionField 以用于 DocBasedVersionConstraintsProcessorFactory 配置中。

DocBasedVersionConstraintsProcessorFactory 支持以下附加配置参数,它们都是可选的

ignoreOldUpdates

可选

默认值:false

如果设置为 true,则将静默忽略更新(并向客户端返回状态 200),而不是拒绝 versionField 过低的更新。

deleteVersionParam

可选

默认值:无

可以指定一个字符串参数以指示此处理器还应检查按 ID 删除命令。

此选项的值应为请求参数的名称,该处理器会将其视为对所有按 ID 删除尝试的强制要求,并且客户端必须使用该名称来指定 versionField 的值,该值大于要删除的文档的现有值。

使用此请求参数时,任何文档版本号足够高以成功执行的“按 ID 删除”命令都将在内部转换为“添加文档”命令,该命令将现有文档替换为一个新文档,该新文档除了唯一键和 versionField 外为空,以保留已删除版本的记录,因此如果其“新”版本不够高,则未来的“添加文档”命令将失败。

如果将 versionField 指定为列表,则此参数也必须指定为相同大小的逗号分隔列表,以便参数与字段对应。

supportMissingVersionOnOldDocs

可选

默认值:false

如果设置为 true,则允许在启用此功能之前编写的任何缺少 versionField 的文档被覆盖。

请参阅 DocBasedVersionConstraintsProcessorFactory javadocs测试 solrconfig.xml 文件 以获取其他信息和示例用法。